rename accounts into account for consistency
[nodemanager.git] / sliver_vs.py
1 #
2
3 """VServer slivers.
4
5 There are a couple of tricky things going on here.  First, the kernel
6 needs disk usage information in order to enforce the quota.  However,
7 determining disk usage redundantly strains the disks.  Thus, the
8 Sliver_VS.disk_usage_initialized flag is used to determine whether
9 this initialization has been made.
10
11 Second, it's not currently possible to set the scheduler parameters
12 for a sliver unless that sliver has a running process.  /bin/vsh helps
13 us out by reading the configuration file so that it can set the
14 appropriate limits after entering the sliver context.  Making the
15 syscall that actually sets the parameters gives a harmless error if no
16 process is running.  Thus we keep vm_running on when setting scheduler
17 parameters so that set_sched_params() always makes the syscall, and we
18 don't have to guess if there is a running process or not.
19 """
20
21 import errno
22 import traceback
23 import os, os.path
24 import sys
25 import time
26 from threading import BoundedSemaphore
27 import subprocess
28
29 # the util-vserver-pl module
30 import vserver
31
32 import account
33 import logger
34 import tools
35
36 # special constant that tells vserver to keep its existing settings
37 KEEP_LIMIT = vserver.VC_LIM_KEEP
38
39 # populate the sliver/vserver specific default allocations table,
40 # which is used to look for slice attributes
41 DEFAULT_ALLOCATION = {}
42 for rlimit in vserver.RLIMITS.keys():
43     rlim = rlimit.lower()
44     DEFAULT_ALLOCATION["%s_min"%rlim]=KEEP_LIMIT
45     DEFAULT_ALLOCATION["%s_soft"%rlim]=KEEP_LIMIT
46     DEFAULT_ALLOCATION["%s_hard"%rlim]=KEEP_LIMIT
47
48 class Sliver_VS(account.Account, vserver.VServer):
49     """This class wraps vserver.VServer to make its interface closer to what we need."""
50
51     SHELL = '/bin/vsh'
52     TYPE = 'sliver.VServer'
53     _init_disk_info_sem = BoundedSemaphore()
54
55     def __init__(self, rec):
56         name=rec['name']
57         logger.verbose ('sliver_vs: %s init'%name)
58         try:
59             logger.log("sliver_vs: %s: first chance..."%name)
60             vserver.VServer.__init__(self, name,logfile='/var/log/nodemanager')
61         except Exception, err:
62             if not isinstance(err, vserver.NoSuchVServer):
63                 # Probably a bad vserver or vserver configuration file
64                 logger.log_exc("sliver_vs:__init__ (first chance) %s",name=name)
65                 logger.log('sliver_vs: %s: recreating bad vserver' % name)
66                 self.destroy(name)
67             self.create(name, rec)
68             logger.log("sliver_vs: %s: second chance..."%name)
69             vserver.VServer.__init__(self, name,logfile='/var/log/nodemanager')
70
71         self.keys = ''
72         self.rspec = {}
73         self.slice_id = rec['slice_id']
74         self.disk_usage_initialized = False
75         self.initscript = ''
76         self.enabled = True
77         self.configure(rec)
78
79     @staticmethod
80     def create(name, rec = None):
81         logger.verbose('sliver_vs: %s: create'%name)
82         vref = rec['vref']
83         if vref is None:
84             logger.log("sliver_vs: %s: ERROR - no vref attached, this is unexpected"%(name))
85             # added by caglar
86             # band-aid for short period as old API doesn't have GetSliceFamily function
87             #return
88             vref = "planetlab-f8-i386"
89
90         # used to look in /etc/planetlab/family,
91         # now relies on the 'GetSliceFamily' extra attribute in GetSlivers()
92         # which for legacy is still exposed here as the 'vref' key
93
94         # check the template exists -- there's probably a better way..
95         if not os.path.isdir ("/vservers/.vref/%s"%vref):
96             logger.log ("sliver_vs: %s: ERROR Could not create sliver - vreference image %s not found"%(name,vref))
97             return
98
99         # guess arch
100         try:
101             (x,y,arch)=vref.split('-')
102         # mh, this of course applies when 'vref' is e.g. 'netflow'
103         # and that's not quite right
104         except:
105             arch='i386'
106
107         def personality (arch):
108             personality="linux32"
109             if arch.find("64")>=0:
110                 personality="linux64"
111             return personality
112
113         command=[]
114         # be verbose
115         command += ['/bin/bash','-x',]
116         command += ['/usr/sbin/vuseradd', ]
117         if 'attributes' in rec and 'isolate_loopback' in rec['attributes'] and rec['attributes']['isolate_loopback'] == '1':
118             command += [ "-i",]
119         # the vsliver imge to use
120         command += [ '-t', vref, ]
121         # slice name
122         command += [ name, ]            
123 #        logger.log_call(['/usr/sbin/vuseradd', '-t', vref, name, ], timeout=15*60)
124         logger.log_call(command, timeout=15*60)
125         # export slicename to the slice in /etc/slicename
126         file('/vservers/%s/etc/slicename' % name, 'w').write(name)
127         file('/vservers/%s/etc/slicefamily' % name, 'w').write(vref)
128         # set personality: only if needed (if arch's differ)
129         if tools.root_context_arch() != arch:
130             file('/etc/vservers/%s/personality' % name, 'w').write(personality(arch)+"\n")
131             logger.log('sliver_vs: %s: set personality to %s'%(name,personality(arch)))
132
133     @staticmethod
134     def destroy(name):
135 #        logger.log_call(['/usr/sbin/vuserdel', name, ])
136         logger.log_call(['/bin/bash','-x','/usr/sbin/vuserdel', name, ])
137
138     def configure(self, rec):
139         # in case we update nodemanager..
140         self.install_and_enable_vinit()
141
142         new_rspec = rec['_rspec']
143         if new_rspec != self.rspec:
144             self.rspec = new_rspec
145             self.set_resources()
146
147         new_initscript = rec['initscript']
148         if new_initscript != self.initscript:
149             self.initscript = new_initscript
150             # not used anymore, we always check against the installed script
151             #self.initscriptchanged = True
152             self.refresh_slice_vinit()
153
154         account.Account.configure(self, rec)  # install ssh keys
155
156     # unconditionnally install and enable the generic vinit script
157     # mimicking chkconfig for enabling the generic vinit script
158     # this is hardwired for runlevel 3
159     def install_and_enable_vinit (self):
160         vinit_source="/usr/share/NodeManager/sliver-initscripts/vinit"
161         vinit_script="/vservers/%s/etc/rc.d/init.d/vinit"%self.name
162         rc3_link="/vservers/%s/etc/rc.d/rc3.d/S99vinit"%self.name
163         rc3_target="../init.d/vinit"
164         # install in sliver
165         code=file(vinit_source).read()
166         if tools.replace_file_with_string(vinit_script,code,chmod=0755):
167             logger.log("vsliver_vs: %s: installed generic vinit rc script"%self.name)
168         # create symlink for runlevel 3
169         if not os.path.islink(rc3_link):
170             try:
171                 logger.log("vsliver_vs: %s: creating runlevel3 symlink %s"%(self.name,rc3_link))
172                 os.symlink(rc3_target,rc3_link)
173             except:
174                 logger.log_exc("vsliver_vs: %s: failed to create runlevel3 symlink %s"%rc3_link)
175
176     def rerun_slice_vinit(self):
177         command = "/usr/sbin/vserver %s exec /etc/rc.d/init.d/vinit restart" % (self.name)
178         logger.log("vsliver_vs: %s: Rerunning slice initscript: %s" % (self.name, command))
179         subprocess.call(command + "&", stdin=open('/dev/null', 'r'), stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT, shell=True)
180
181     # this one checks for the existence of the slice initscript
182     # install or remove the slice inistscript, as instructed by the initscript tag
183     def refresh_slice_vinit(self):
184         code=self.initscript
185         sliver_initscript="/vservers/%s/etc/rc.d/init.d/vinit.slice"%self.name
186         if tools.replace_file_with_string(sliver_initscript,code,remove_if_empty=True,chmod=0755):
187             if code:
188                 logger.log("vsliver_vs: %s: Installed new initscript in %s"%(self.name,sliver_initscript))
189                 if self.is_running():
190                     # Only need to rerun the initscript if the vserver is
191                     # already running. If the vserver isn't running, then the
192                     # initscript will automatically be started by
193                     # /etc/rc.d/vinit when the vserver is started.
194                     self.rerun_slice_vinit()
195             else:
196                 logger.log("vsliver_vs: %s: Removed obsolete initscript %s"%(self.name,sliver_initscript))
197
198     def start(self, delay=0):
199         if self.rspec['enabled'] <= 0:
200             logger.log('sliver_vs: not starting %s, is not enabled'%self.name)
201         else:
202             logger.log('sliver_vs: %s: starting in %d seconds' % (self.name, delay))
203             time.sleep(delay)
204             # the generic /etc/init.d/vinit script is permanently refreshed, and enabled
205             self.install_and_enable_vinit()
206             # expose .ssh for omf_friendly slivers
207             if 'omf_control' in self.rspec['tags']:
208                 self.expose_ssh_dir()
209             # if a change has occured in the slice initscript, reflect this in /etc/init.d/vinit.slice
210             self.refresh_slice_vinit()
211             child_pid = os.fork()
212             if child_pid == 0:
213                 # VServer.start calls fork() internally,
214                 # so just close the nonstandard fds and fork once to avoid creating zombies
215                 tools.close_nonstandard_fds()
216                 vserver.VServer.start(self)
217                 os._exit(0)
218             else:
219                 os.waitpid(child_pid, 0)
220
221     def stop(self):
222         logger.log('sliver_vs: %s: stopping' % self.name)
223         vserver.VServer.stop(self)
224
225     def is_running(self):
226         return vserver.VServer.is_running(self)
227
228     def set_resources(self):
229         disk_max = self.rspec['disk_max']
230         logger.log('sliver_vs: %s: setting max disk usage to %d KiB' % (self.name, disk_max))
231         try:  # if the sliver is over quota, .set_disk_limit will throw an exception
232             if not self.disk_usage_initialized:
233                 self.vm_running = False
234                 Sliver_VS._init_disk_info_sem.acquire()
235                 logger.log('sliver_vs: %s: computing disk usage: beginning' % self.name)
236                 # init_disk_info is inherited from VServer
237                 try: self.init_disk_info()
238                 finally: Sliver_VS._init_disk_info_sem.release()
239                 logger.log('sliver_vs: %s: computing disk usage: ended' % self.name)
240                 self.disk_usage_initialized = True
241             vserver.VServer.set_disklimit(self, max(disk_max, self.disk_blocks))
242         except:
243             logger.log_exc('sliver_vs: failed to set max disk usage',name=self.name)
244
245         # get/set the min/soft/hard values for all of the vserver
246         # related RLIMITS.  Note that vserver currently only
247         # implements support for hard limits.
248         for limit in vserver.RLIMITS.keys():
249             type = limit.lower()
250             minimum  = self.rspec['%s_min'%type]
251             soft = self.rspec['%s_soft'%type]
252             hard = self.rspec['%s_hard'%type]
253             update = self.set_rlimit(limit, hard, soft, minimum)
254             if update:
255                 logger.log('sliver_vs: %s: setting rlimit %s to (%d, %d, %d)'
256                            % (self.name, type, hard, soft, minimum))
257
258         self.set_capabilities_config(self.rspec['capabilities'])
259         if self.rspec['capabilities']:
260             logger.log('sliver_vs: %s: setting capabilities to %s' % (self.name, self.rspec['capabilities']))
261
262         cpu_pct = self.rspec['cpu_pct']
263         cpu_share = self.rspec['cpu_share']
264
265         count = 1
266         for key in self.rspec.keys():
267             if key.find('sysctl.') == 0:
268                 sysctl=key.split('.')
269                 try:
270                     # /etc/vservers/<guest>/sysctl/<id>/
271                     dirname = "/etc/vservers/%s/sysctl/%s" % (self.name, count)
272                     try:
273                         os.makedirs(dirname, 0755)
274                     except:
275                         pass
276                     setting = open("%s/setting" % dirname, "w")
277                     setting.write("%s\n" % key.lstrip("sysctl."))
278                     setting.close()
279                     value = open("%s/value" % dirname, "w")
280                     value.write("%s\n" % self.rspec[key])
281                     value.close()
282                     count += 1
283
284                     logger.log("sliver_vs: %s: writing %s=%s"%(self.name,key,self.rspec[key]))
285                 except IOError, e:
286                     logger.log("sliver_vs: %s: could not set %s=%s"%(self.name,key,self.rspec[key]))
287                     logger.log("sliver_vs: %s: error = %s"%(self.name,e))
288
289
290         if self.rspec['enabled'] > 0:
291             if cpu_pct > 0:
292                 logger.log('sliver_vs: %s: setting cpu reservation to %d%%' % (self.name, cpu_pct))
293             else:
294                 cpu_pct = 0
295
296             if cpu_share > 0:
297                 logger.log('sliver_vs: %s: setting cpu share to %d' % (self.name, cpu_share))
298             else:
299                 cpu_share = 0
300
301             self.set_sched_config(cpu_pct, cpu_share)
302             # if IP address isn't set (even to 0.0.0.0), sliver won't be able to use network
303             if self.rspec['ip_addresses'] != '0.0.0.0':
304                 logger.log('sliver_vs: %s: setting IP address(es) to %s' % \
305                 (self.name, self.rspec['ip_addresses']))
306             add_loopback = True
307             if 'isolate_loopback' in self.rspec['tags']:
308                 add_loopback = self.rspec['tags']['isolate_loopback'] != "1"
309             self.set_ipaddresses_config(self.rspec['ip_addresses'], add_loopback)
310
311             #logger.log("sliver_vs: %s: Setting name to %s" % (self.name, self.slice_id))
312             #self.setname(self.slice_id)
313             #logger.log("sliver_vs: %s: Storing slice id of %s for PlanetFlow" % (self.name, self.slice_id))
314             try:
315                 vserver_config_path = '/etc/vservers/%s'%self.name
316                 if not os.path.exists (vserver_config_path):
317                     os.makedirs (vserver_config_path)
318                 file('%s/slice_id'%vserver_config_path, 'w').write("%d\n"%self.slice_id)
319                 logger.log("sliver_vs: Recorded slice id %d for slice %s"%(self.slice_id,self.name))
320             except IOError,e:
321                 logger.log("sliver_vs: Could not record slice_id for slice %s. Error: %s"%(self.name,str(e)))
322             except Exception,e:
323                 logger.log_exc("sliver_vs: Error recording slice id: %s"%str(e),name=self.name)
324
325
326             if self.enabled == False:
327                 self.enabled = True
328                 self.start()
329
330             if False: # Does not work properly yet.
331                 if self.have_limits_changed():
332                     logger.log('sliver_vs: %s: limits have changed --- restarting' % self.name)
333                     stopcount = 10
334                     while self.is_running() and stopcount > 0:
335                         self.stop()
336                         delay = 1
337                         time.sleep(delay)
338                         stopcount = stopcount - 1
339                     self.start()
340
341         else:  # tell vsh to disable remote login by setting CPULIMIT to 0
342             logger.log('sliver_vs: %s: disabling remote login' % self.name)
343             self.set_sched_config(0, 0)
344             self.enabled = False
345             self.stop()