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