Revert to forking before calling VServer.start() to avoid creating zombies.
[nodemanager.git] / sliver_vs.py
1 """VServer slivers.
2
3 There are a couple of tricky things going on here.  First, the kernel
4 needs disk usage information in order to enforce the quota.  However,
5 determining disk usage redundantly strains the disks.  Thus, the
6 Sliver_VS.disk_usage_initialized flag is used to determine whether
7 this initialization has been made.
8
9 Second, it's not currently possible to set the scheduler parameters
10 for a sliver unless that sliver has a running process.  /bin/vsh helps
11 us out by reading the configuration file so that it can set the
12 appropriate limits after entering the sliver context.  Making the
13 syscall that actually sets the parameters gives a harmless error if no
14 process is running.  Thus we keep vm_running on when setting scheduler
15 parameters so that set_sched_params() always makes the syscall, and we
16 don't have to guess if there is a running process or not.
17 """
18
19 import errno
20 import os, os.path
21 import time
22 import vserver
23
24 import accounts
25 import logger
26 import tools
27 from threading import BoundedSemaphore
28
29 globalsem = BoundedSemaphore()
30
31 # special constant that tells vserver to keep its existing settings
32 KEEP_LIMIT = vserver.VC_LIM_KEEP
33
34 # populate the sliver/vserver specific default allocations table,
35 # which is used to look for slice attributes
36 DEFAULT_ALLOCATION = {}
37 for rlimit in vserver.RLIMITS.keys():
38     rlim = rlimit.lower()
39     DEFAULT_ALLOCATION["%s_min"%rlim]=KEEP_LIMIT
40     DEFAULT_ALLOCATION["%s_soft"%rlim]=KEEP_LIMIT
41     DEFAULT_ALLOCATION["%s_hard"%rlim]=KEEP_LIMIT
42
43 class Sliver_VS(accounts.Account, vserver.VServer):
44     """This class wraps vserver.VServer to make its interface closer to what we need."""
45
46     SHELL = '/bin/vsh'
47     TYPE = 'sliver.VServer'
48     _init_disk_info_sem = globalsem
49
50     def __init__(self, rec):
51         logger.verbose ('initing Sliver_VS with name=%s'%rec['name'])
52         try:
53             vserver.VServer.__init__(self, rec['name'],logfile='/var/log/nm')
54         except Exception, err:
55             if not isinstance(err, vserver.NoSuchVServer):
56                 # Probably a bad vserver or vserver configuration file
57                 logger.log_exc(self.name)
58                 logger.log('%s: recreating bad vserver' % rec['name'])
59                 self.destroy(rec['name'])
60             self.create(rec['name'], rec['vref'])
61             vserver.VServer.__init__(self, rec['name'],logfile='/var/log/nm')
62
63         self.keys = ''
64         self.rspec = {}
65         self.initscript = ''
66         self.slice_id = rec['slice_id']
67         self.disk_usage_initialized = False
68         self.initscriptchanged = False
69         self.configure(rec)
70
71     @staticmethod
72     def create(name, vref = None):
73         logger.verbose('Sliver_VS:create - name=%s'%name)
74         if vref is None:
75             vref='default'
76         try:
77             ### locating the right slicefamily
78             # this is a first draft, and more a proof of concept thing
79             # the idea is to parse vref for dash-separated wishes,
80             # and to project these against the defaults
81             # so e.g. if the default slice family (as found in /etc/planetlab/slicefamily)
82             # is planetlab-f8-i386, then here is what we get
83             # vref=x86_64             -> vuseradd -t planetlab-f8-x86_64 
84             # vref=centos5            -> vuseradd -t planetlab-centos5-i386 
85             # vref=centos5-onelab     -> vuseradd -t onelab-centos5-i386 
86             # vref=planetflow         -> vuseradd -t planetflow-f8-i386
87             # vref=x86_64-planetflow  -> vuseradd -t planetflow-f8-x86_64
88
89             # default
90             default=file("/etc/planetlab/slicefamily").read().strip()
91             (pldistro,fcdistro,arch) = default.split("-")
92
93             known_archs = [ 'i386', 'x86_64' ]
94             known_fcdistros = [ 'f8', 'f9', 'centos5' ]
95             # from the slice attribute: cut dashes and try to figure the meaning
96             slice_wishes = vref.split("-")
97             for wish in slice_wishes:
98                 if wish in known_archs:
99                     arch=wish
100                 elif wish in known_fcdistros:
101                     fcdistro=wish
102                 else:
103                     pldistro=wish
104
105             # rejoin the parts
106             refname="-".join( (pldistro,fcdistro,arch) )
107
108             # check the templates exists -- there's probably a better way..
109             if not os.path.isdir ("/vservers/.vref/%s"%refname):
110                 logger.verbose("%s (%s) : vref %s not found, using default %s"%(
111                         name,vref,refname,default))
112                 refname=default
113                 # could check again, but as we have /etc/slicefamily 
114                 # there's probably no /vservers/.vref/default
115
116         except IOError:
117             # have not found slicefamily
118             logger.verbose("%s (%s): legacy node - using fallback vrefname 'default'"%(name,vref))
119                 # for legacy nodes
120             refname="default"
121         except:
122             import traceback
123             logger.log("%s (%s) : unexpected error follows - using 'default'"%(
124                     name,vref))
125             logger.log(traceback.format_exc())
126             refname="default"
127             
128         logger.log_call('/usr/sbin/vuseradd', '-t', refname, name)
129         open('/vservers/%s/etc/slicename' % name, 'w').write(name)
130
131     @staticmethod
132     def destroy(name): logger.log_call('/usr/sbin/vuserdel', name)
133
134     def configure(self, rec):
135         new_rspec = rec['_rspec']
136         if new_rspec != self.rspec:
137             self.rspec = new_rspec
138             self.set_resources()
139
140         new_initscript = rec['initscript']
141         if new_initscript != self.initscript:
142             self.initscript = new_initscript
143             logger.log('%s: installing initscript' % self.name)
144             def install_initscript():
145                 flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
146                 fd = os.open('/etc/rc.vinit', flags, 0755)
147                 os.write(fd, new_initscript)
148                 os.close(fd)
149             try:
150                 self.chroot_call(install_initscript)
151                 self.initscriptchanged = True
152             except: logger.log_exc(self.name)
153
154         accounts.Account.configure(self, rec)  # install ssh keys
155
156     def start(self, delay=0):
157         if self.rspec['enabled'] > 0:
158             logger.log('%s: starting in %d seconds' % (self.name, delay))
159             time.sleep(delay)
160             child_pid = os.fork()
161             if child_pid == 0:
162                 # VServer.start calls fork() internally, 
163                 # so just close the nonstandard fds and fork once to avoid creating zombies
164                 tools.close_nonstandard_fds()
165                 vserver.VServer.start(self)
166                 os._exit(0)
167             else: os.waitpid(child_pid, 0)
168         else: logger.log('%s: not starting, is not enabled' % self.name)
169         self.initscriptchanged = False
170
171     def stop(self):
172         logger.log('%s: stopping' % self.name)
173         vserver.VServer.stop(self)
174
175     def set_resources(self):
176         disk_max = self.rspec['disk_max']
177         logger.log('%s: setting max disk usage to %d KiB' % (self.name, disk_max))
178         try:  # if the sliver is over quota, .set_disk_limit will throw an exception
179             if not self.disk_usage_initialized:
180                 self.vm_running = False
181                 logger.log('%s: computing disk usage: beginning' % self.name)
182                 Sliver_VS._init_disk_info_sem.acquire()
183                 try: self.init_disk_info()
184                 finally: Sliver_VS._init_disk_info_sem.release()
185                 logger.log('%s: computing disk usage: ended' % self.name)
186                 self.disk_usage_initialized = True
187             vserver.VServer.set_disklimit(self, max(disk_max, self.disk_blocks))
188         except:
189             logger.log('%s: failed to set max disk usage' % self.name)
190             logger.log_exc(self.name)
191
192         # get/set the min/soft/hard values for all of the vserver
193         # related RLIMITS.  Note that vserver currently only
194         # implements support for hard limits.
195         for limit in vserver.RLIMITS.keys():
196             type = limit.lower()
197             minimum  = self.rspec['%s_min'%type]
198             soft = self.rspec['%s_soft'%type]
199             hard = self.rspec['%s_hard'%type]
200             update = self.set_rlimit(limit, hard, soft, minimum)
201             if update:
202                 logger.log('%s: setting rlimit %s to (%d, %d, %d)'
203                            % (self.name, type, hard, soft, minimum))
204
205         self.set_capabilities_config(self.rspec['capabilities'])
206         if self.rspec['capabilities']:
207             logger.log('%s: setting capabilities to %s' % (self.name, self.rspec['capabilities']))
208
209         cpu_pct = self.rspec['cpu_pct']
210         cpu_share = self.rspec['cpu_share']
211
212         if self.rspec['enabled'] > 0:
213             if cpu_pct > 0:
214                 logger.log('%s: setting cpu reservation to %d%%' % (self.name, cpu_pct))
215             else:
216                 cpu_pct = 0
217
218             if cpu_share > 0:
219                 logger.log('%s: setting cpu share to %d' % (self.name, cpu_share))
220             else:
221                 cpu_share = 0
222
223             self.set_sched_config(cpu_pct, cpu_share)
224             # if IP address isn't set (even to 0.0.0.0), sliver won't be able to use network
225             if self.rspec['ip_addresses'] != '0.0.0.0':
226                 logger.log('%s: setting IP address(es) to %s' % \
227                 (self.name, self.rspec['ip_addresses']))
228             self.set_ipaddresses_config(self.rspec['ip_addresses'])
229
230             if self.is_running():
231                 logger.log("%s: Setting name to %s" % (self.name, self.slice_id),2)
232                 self.setname(self.slice_id)
233  
234             if False: # Does not work properly yet.
235                 if self.have_limits_changed():
236                     logger.log('%s: limits have changed --- restarting' % self.name)
237                     stopcount = 10
238                     while self.is_running() and stopcount > 0:
239                         self.stop()
240                         delay = 1
241                         time.sleep(delay)
242                         stopcount = stopcount - 1
243                     self.start()
244
245         else:  # tell vsh to disable remote login by setting CPULIMIT to 0
246             logger.log('%s: disabling remote login' % self.name)
247             self.set_sched_config(0, 0)
248             self.stop()