Don't wait for the initscripts to complete.
[util-vserver.git] / python / vserver.py
1 # Copyright 2005 Princeton University
2
3 #$Id: vserver.py,v 1.72 2007/08/02 16:01:59 dhozac Exp $
4
5 import errno
6 import fcntl
7 import os
8 import re
9 import pwd
10 import signal
11 import sys
12 import time
13 import traceback
14 import subprocess
15 import resource
16
17 import vserverimpl
18 import cpulimit, bwlimit
19
20 from vserverimpl import VS_SCHED_CPU_GUARANTEED as SCHED_CPU_GUARANTEED
21 from vserverimpl import DLIMIT_INF
22 from vserverimpl import VC_LIM_KEEP
23 from vserverimpl import VLIMIT_NSOCK
24 from vserverimpl import VLIMIT_OPENFD
25 from vserverimpl import VLIMIT_ANON
26 from vserverimpl import VLIMIT_SHMEM
27
28 #
29 # these are the flags taken from the kernel linux/vserver/legacy.h
30 #
31 FLAGS_LOCK = 1
32 FLAGS_SCHED = 2  # XXX - defined in util-vserver/src/chcontext.c
33 FLAGS_NPROC = 4
34 FLAGS_PRIVATE = 8
35 FLAGS_INIT = 16
36 FLAGS_HIDEINFO = 32
37 FLAGS_ULIMIT = 64
38 FLAGS_NAMESPACE = 128
39
40 RLIMITS = { "NSOCK": VLIMIT_NSOCK,
41             "OPENFD": VLIMIT_OPENFD,
42             "ANON": VLIMIT_ANON,
43             "SHMEM": VLIMIT_SHMEM}
44
45 # add in the platform supported rlimits
46 for entry in resource.__dict__.keys():
47     if entry.find("RLIMIT_")==0:
48         k = entry[len("RLIMIT_"):]
49         if not RLIMITS.has_key(k):
50             RLIMITS[k]=resource.__dict__[entry]
51         else:
52             print "WARNING: duplicate RLIMITS key %s" % k
53
54 class NoSuchVServer(Exception): pass
55
56
57 class VServerConfig:
58     def __init__(self, name, directory):
59         self.name = name
60         self.dir = directory
61         self.cache = None
62         if not (os.path.isdir(self.dir) and
63                 os.access(self.dir, os.R_OK | os.W_OK | os.X_OK)):
64             raise NoSuchVServer, "%s does not exist" % self.dir
65
66     def get(self, option, default = None):
67         try:
68             if self.cache:
69                 return self.cache[option]
70             else:
71                 f = open(os.path.join(self.dir, option), "r")
72                 buf = f.read().rstrip()
73                 f.close()
74                 return buf
75         except:
76             if default is not None:
77                 return default
78             else:
79                 raise KeyError, "Key %s is not set for %s" % (option, self.name)
80
81     def update(self, option, value):
82         if self.cache:
83             return
84
85         try:
86             old_umask = os.umask(0022)
87             filename = os.path.join(self.dir, option)
88             try:
89                 os.makedirs(os.path.dirname(filename), 0755)
90             except:
91                 pass
92             f = open(filename, 'w')
93             if isinstance(value, list):
94                 f.write("%s\n" % "\n".join(value))
95             else:
96                 f.write("%s\n" % value)
97             f.close()
98             os.umask(old_umask)
99         except:
100             raise
101
102     def unset(self, option):
103         if self.cache:
104             return
105
106         try:
107             filename = os.path.join(self.dir, option)
108             os.unlink(filename)
109             try:
110                 os.removedirs(os.path.dirname(filename))
111             except:
112                 pass
113             return True
114         except:
115             return False
116
117     def cache_it(self):
118         self.cache = {}
119         def add_to_cache(cache, dirname, fnames):
120             for file in fnames:
121                 full_name = os.path.join(dirname, file)
122                 if os.path.islink(full_name):
123                     fnames.remove(file)
124                 elif (os.path.isfile(full_name) and
125                       os.access(full_name, os.R_OK)):
126                     f = open(full_name, "r")
127                     cache[full_name.replace(os.path.join(self.dir, ''),
128                                             '')] = f.read().rstrip()
129                     f.close()
130         os.path.walk(self.dir, add_to_cache, self.cache)
131
132
133 class VServer:
134
135     INITSCRIPTS = [('/etc/rc.vinit', 'start'),
136                    ('/etc/rc.d/rc', '%(runlevel)d')]
137
138     def __init__(self, name, vm_id = None, vm_running = None):
139
140         self.name = name
141         self.rlimits_changed = False
142         self.dir = "%s/%s" % (vserverimpl.VSERVER_BASEDIR, name)
143         if not (os.path.isdir(self.dir) and
144                 os.access(self.dir, os.R_OK | os.W_OK | os.X_OK)):
145             raise NoSuchVServer, "no such vserver: " + name
146         self.config = VServerConfig(name, "/etc/vservers/%s" % name)
147         self.remove_caps = ~vserverimpl.CAP_SAFE;
148         if vm_id == None:
149             vm_id = int(self.config.get('context'))
150         self.ctx = vm_id
151         if vm_running == None:
152             vm_running = self.is_running()
153         self.vm_running = vm_running
154
155     def have_limits_changed(self):
156         return self.rlimits_changed
157
158     def set_rlimit_limit(self,type,hard,soft,minimum):
159         """Generic set resource limit function for vserver"""
160         global RLIMITS
161         changed = False
162         try:
163             old_hard, old_soft, old_minimum = self.get_rlimit_limit(type)
164             if old_hard != VC_LIM_KEEP and old_hard <> hard: changed = True
165             if old_soft != VC_LIM_KEEP and old_soft <> soft: changed = True
166             if old_minimum != VC_LIM_KEEP and old_minimum <> minimum: changed = True
167             self.rlimits_changed = self.rlimits_changed or changed 
168         except OSError, e:
169             if self.is_running(): print "Unexpected error with getrlimit for running context %d" % self.ctx
170
171         resource_type = RLIMITS[type]
172         try:
173             ret = vserverimpl.setrlimit(self.ctx,resource_type,hard,soft,minimum)
174         except OSError, e:
175             if self.is_running(): print "Unexpected error with setrlimit for running context %d" % self.ctx
176
177     def set_rlimit_config(self,type,hard,soft,minimum):
178         """Generic set resource limit function for vserver"""
179         if hard <> VC_LIM_KEEP:
180             self.config.update('rlimits/%s.hard' % type.lower(), hard)
181         if soft <> VC_LIM_KEEP:
182             self.config.update('rlimits/%s.soft' % type.lower(), soft)
183         if minimum <> VC_LIM_KEEP:
184             self.config.update('rlimits/%s.min' % type.lower(), minimum)
185         self.set_rlimit_limit(type,hard,soft,minimum)
186
187     def get_rlimit_limit(self,type):
188         """Generic get resource configuration function for vserver"""
189         global RLIMITS
190         resource_type = RLIMITS[type]
191         try:
192             ret = vserverimpl.getrlimit(self.ctx,resource_type)
193         except OSError, e:
194             print "Unexpected error with getrlimit for context %d" % self.ctx
195             ret = self.get_rlimit_config(type)
196         return ret
197
198     def get_rlimit_config(self,type):
199         """Generic get resource configuration function for vserver"""
200         hard = int(self.config.get("rlimits/%s.hard"%type.lower(),VC_LIM_KEEP))
201         soft = int(self.config.get("rlimits/%s.soft"%type.lower(),VC_LIM_KEEP))
202         minimum = int(self.config.get("rlimits/%s.min"%type.lower(),VC_LIM_KEEP))
203         return (hard,soft,minimum)
204
205     def set_capabilities(self, capabilities):
206         return vserverimpl.setbcaps(self.ctx, vserverimpl.text2bcaps(capabilities))
207
208     def set_capabilities_config(self, capabilities):
209         self.config.update('bcapabilities', capabilities)
210         self.set_capabilities(capabilities)
211
212     def get_capabilities(self):
213         return vserverimpl.bcaps2text(vserverimpl.getbcaps(self.ctx))
214  
215     def get_capabilities_config(self):
216         return self.config.get('bcapabilities', '')
217
218     def set_ipaddresses(self, addresses):
219         vserverimpl.netremove(self.ctx, "all")
220         for a in addresses.split(","):
221             vserverimpl.netadd(self.ctx, a)
222
223     def set_ipaddresses_config(self, addresses):
224         i = 0
225         for a in addresses.split(","):
226             self.config.update("interfaces/%d/ip" % i, a)
227             i += 1
228         while self.config.unset("interfaces/%d/ip" % i):
229             i += 1
230         self.set_ipaddresses(addresses)
231
232     def get_ipaddresses_config(self):
233         i = 0
234         ret = []
235         while True:
236             r = self.config.get("interfaces/%d/ip" % i, '')
237             if r == '':
238                 break
239             ret += [r]
240             i += 1
241         return ",".join(ret)
242
243     def get_ipaddresses(self):
244         # No clean way to do this right now.
245         return None
246
247     def __do_chroot(self):
248         self.config.cache_it()
249         os.chroot(self.dir)
250         os.chdir("/")
251
252     def chroot_call(self, fn, *args):
253
254         cwd_fd = os.open(".", os.O_RDONLY)
255         try:
256             root_fd = os.open("/", os.O_RDONLY)
257             try:
258                 self.__do_chroot()
259                 result = fn(*args)
260             finally:
261                 os.fchdir(root_fd)
262                 os.chroot(".")
263                 os.fchdir(cwd_fd)
264                 os.close(root_fd)
265         finally:
266             os.close(cwd_fd)
267         return result
268
269     def set_disklimit(self, block_limit):
270         # block_limit is in kB
271         if block_limit == 0:
272             try:
273                 vserverimpl.unsetdlimit(self.dir, self.ctx)
274             except OSError, e:
275                 print "Unexpected error with unsetdlimit for context %d" % self.ctx
276             return
277
278         if self.vm_running:
279             block_usage = vserverimpl.DLIMIT_KEEP
280             inode_usage = vserverimpl.DLIMIT_KEEP
281         else:
282             # init_disk_info() must have been called to get usage values
283             block_usage = self.disk_blocks
284             inode_usage = self.disk_inodes
285
286
287         try:
288             vserverimpl.setdlimit(self.dir,
289                                   self.ctx,
290                                   block_usage,
291                                   block_limit,
292                                   inode_usage,
293                                   vserverimpl.DLIMIT_INF,  # inode limit
294                                   2)   # %age reserved for root
295         except OSError, e:
296             print "Unexpected error with setdlimit for context %d" % self.ctx
297
298
299         self.config.update('dlimits/0/space_total', block_limit)
300
301     def is_running(self):
302         return vserverimpl.isrunning(self.ctx)
303     
304     def get_disklimit(self):
305
306         try:
307             (self.disk_blocks, block_limit, self.disk_inodes, inode_limit,
308              reserved) = vserverimpl.getdlimit(self.dir, self.ctx)
309         except OSError, ex:
310             if ex.errno != errno.ESRCH:
311                 raise
312             # get here if no vserver disk limit has been set for xid
313             block_limit = -1
314
315         return block_limit
316
317     def set_sched_config(self, cpu_share, sched_flags):
318
319         """ Write current CPU scheduler parameters to the vserver
320         configuration file. This method does not modify the kernel CPU
321         scheduling parameters for this context. """
322
323         if sched_flags & SCHED_CPU_GUARANTEED:
324             cpu_guaranteed = cpu_share
325         else:
326             cpu_guaranteed = 0
327         self.config.update('sched/fill-rate2', cpu_share)
328         self.config.update('sched/fill-rate', cpu_guaranteed)
329
330         if self.vm_running:
331             self.set_sched(cpu_share, sched_flags)
332
333     def set_sched(self, cpu_share, sched_flags = 0):
334         """ Update kernel CPU scheduling parameters for this context. """
335         vserverimpl.setsched(self.ctx, cpu_share, sched_flags)
336
337     def get_sched(self):
338         # have no way of querying scheduler right now on a per vserver basis
339         return (-1, False)
340
341     def set_bwlimit(self, minrate = bwlimit.bwmin, maxrate = None,
342                     exempt_min = None, exempt_max = None,
343                     share = None, dev = "eth0"):
344
345         if minrate is None:
346             bwlimit.off(self.ctx, dev)
347         else:
348             bwlimit.on(self.ctx, dev, share,
349                        minrate, maxrate, exempt_min, exempt_max)
350
351     def get_bwlimit(self, dev = "eth0"):
352
353         result = bwlimit.get(self.ctx)
354         # result of bwlimit.get is (ctx, share, minrate, maxrate)
355         if result:
356             result = result[1:]
357         return result
358
359     def open(self, filename, mode = "r", bufsize = -1):
360
361         return self.chroot_call(open, filename, mode, bufsize)
362
363     def __do_chcontext(self, state_file):
364
365         if state_file:
366             print >>state_file, "%u" % self.ctx
367             state_file.close()
368
369         if vserverimpl.chcontext(self.ctx, vserverimpl.text2bcaps(self.get_capabilities_config())):
370             self.set_resources()
371             vserverimpl.setup_done(self.ctx)
372
373     def __prep(self, runlevel, log):
374
375         """ Perform all the crap that the vserver script does before
376         actually executing the startup scripts. """
377
378         # remove /var/run and /var/lock/subsys files
379         # but don't remove utmp from the top-level /var/run
380         RUNDIR = "/var/run"
381         LOCKDIR = "/var/lock/subsys"
382         filter_fn = lambda fs: filter(lambda f: f != 'utmp', fs)
383         garbage = reduce((lambda (out, ff), (dir, subdirs, files):
384                           (out + map((dir + "/").__add__, ff(files)),
385                            lambda fs: fs)),
386                          list(os.walk(RUNDIR)),
387                          ([], filter_fn))[0]
388         garbage += filter(os.path.isfile, map((LOCKDIR + "/").__add__,
389                                               os.listdir(LOCKDIR)))
390         if False:
391             for f in garbage:
392                 os.unlink(f)
393
394         # set the initial runlevel
395         f = open(RUNDIR + "/utmp", "w")
396         vserverimpl.setrunlevel(f, runlevel)
397         f.close()
398
399         # mount /proc and /dev/pts
400         self.__do_mount("none", self.dir, "/proc", "proc")
401         # XXX - magic mount options
402         self.__do_mount("none", self.dir, "/dev/pts", "devpts", 0, "gid=5,mode=0620")
403
404     def __do_mount(self, *mount_args):
405
406         try:
407             vserverimpl.mount(*mount_args)
408         except OSError, ex:
409             if ex.errno == errno.EBUSY:
410                 # assume already mounted
411                 return
412             raise ex
413
414     def enter(self):
415         self.__do_chroot()
416         self.__do_chcontext(None)
417
418     def start(self, wait, runlevel = 3):
419         self.vm_running = True
420         self.rlimits_changed = False
421
422         child_pid = os.fork()
423         if child_pid == 0:
424             # child process
425             try:
426                 # get a new session
427                 os.setsid()
428
429                 # open state file to record vserver info
430                 state_file = open("/var/run/vservers/%s" % self.name, "w")
431
432                 # use /dev/null for stdin, /var/log/boot.log for stdout/err
433                 fd = os.open("/dev/null", os.O_RDONLY)
434                 if fd != 0:
435                     os.dup2(fd, 0)
436                     os.close(fd)
437                 self.__do_chroot()
438                 log = open("/var/log/boot.log", "w", 0)
439                 if log.fileno() != 1:
440                     os.dup2(log.fileno(), 1)
441                 os.dup2(1, 2)
442
443                 print >>log, ("%s: starting the virtual server %s" %
444                               (time.asctime(time.gmtime()), self.name))
445
446                 # perform pre-init cleanup
447                 self.__prep(runlevel, log)
448
449                 # execute each init script in turn
450                 # XXX - we don't support all scripts that vserver script does
451                 self.__do_chcontext(state_file)
452                 for cmd in self.INITSCRIPTS:
453                      try:
454                          # enter vserver context
455                          arg_subst = { 'runlevel': runlevel }
456                          cmd_args = [cmd[0]] + map(lambda x: x % arg_subst,
457                                                    cmd[1:])
458                          print >>log, "executing '%s'" % " ".join(cmd_args)
459                          os.spawnvp(os.P_NOWAIT,cmd[0],cmd_args)
460                      except:
461                          traceback.print_exc()
462                          os._exit(1)
463
464             # we get here due to an exception in the top-level child process
465             except Exception, ex:
466                 traceback.print_exc()
467             os._exit(0)
468
469         # parent process
470         return child_pid
471
472     def set_resources(self):
473
474         """ Called when vserver context is entered for first time,
475         should be overridden by subclass. """
476
477         pass
478
479     def init_disk_info(self):
480         cmd = "/usr/sbin/vdu --script --space --inodes --blocksize 1024 --xid %d %s" % (self.ctx, self.dir)
481         p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
482                              stdout=subprocess.PIPE, stderr=subprocess.PIPE,
483                              close_fds=True)
484         p.stdin.close()
485         line = p.stdout.readline()
486         if not line:
487             sys.stderr.write(p.stderr.read())
488         p.stdout.close()
489         p.stderr.close()
490         ret = p.wait()
491
492         (space, inodes) = line.split()
493         self.disk_inodes = int(inodes)
494         self.disk_blocks = int(space)
495         #(self.disk_inodes, self.disk_blocks) = vduimpl.vdu(self.dir)
496
497         return self.disk_blocks * 1024
498
499     def stop(self, signal = signal.SIGKILL):
500         vserverimpl.killall(self.ctx, signal)
501         self.vm_running = False
502         self.rlimits_changed = False
503
504
505
506 def create(vm_name, static = False, ctor = VServer):
507
508     options = ['vuseradd']
509     if static:
510         options += ['--static']
511     ret = os.spawnvp(os.P_WAIT, 'vuseradd', options + [vm_name])
512     if !os.WIFEXITED(ret) || os.WEXITSTATUS(ret) != 0:
513         out = "system command ('%s') " % options
514         if os.WIFEXITED(ret):
515             out += "failed, rc = %d" % os.WEXITSTATUS(ret)
516         else:
517             out += "killed by signal %d" % os.WTERMSIG(ret)
518         raise SystemError, out
519     vm_id = pwd.getpwnam(vm_name)[2]
520
521     return ctor(vm_name, vm_id)