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