add support for bcapabilities
[util-vserver.git] / python / vserver.py
1 # Copyright 2005 Princeton University
2
3 import errno
4 import fcntl
5 import os
6 import re
7 import pwd
8 import signal
9 import sys
10 import time
11 import traceback
12
13 import mountimpl
14 import runcmd
15 import utmp
16 import vserverimpl
17 import cpulimit, bwlimit
18
19 from vserverimpl import VS_SCHED_CPU_GUARANTEED as SCHED_CPU_GUARANTEED
20 from vserverimpl import DLIMIT_INF
21 from vserverimpl import VC_LIM_KEEP
22
23 from vserverimpl import RLIMIT_CPU
24 from vserverimpl import RLIMIT_RSS
25 from vserverimpl import RLIMIT_NPROC
26 from vserverimpl import RLIMIT_NOFILE
27 from vserverimpl import RLIMIT_MEMLOCK
28 from vserverimpl import RLIMIT_AS
29 from vserverimpl import RLIMIT_LOCKS
30 from vserverimpl import RLIMIT_SIGPENDING
31 from vserverimpl import RLIMIT_MSGQUEUE
32 from vserverimpl import VLIMIT_NSOCK
33 from vserverimpl import VLIMIT_OPENFD
34 from vserverimpl import VLIMIT_ANON
35 from vserverimpl import VLIMIT_SHMEM
36
37 #
38 # these are the flags taken from the kernel linux/vserver/legacy.h
39 #
40 FLAGS_LOCK = 1
41 FLAGS_SCHED = 2  # XXX - defined in util-vserver/src/chcontext.c
42 FLAGS_NPROC = 4
43 FLAGS_PRIVATE = 8
44 FLAGS_INIT = 16
45 FLAGS_HIDEINFO = 32
46 FLAGS_ULIMIT = 64
47 FLAGS_NAMESPACE = 128
48
49 RLIMITS = {"CPU": RLIMIT_CPU,
50            "RSS": RLIMIT_RSS,
51            "NPROC": RLIMIT_NPROC,
52            "NOFILE": RLIMIT_NOFILE,
53            "MEMLOCK": RLIMIT_MEMLOCK,
54            "AS": RLIMIT_AS,
55            "LOCKS": RLIMIT_LOCKS,
56            "SIGPENDING": RLIMIT_SIGPENDING,
57            "MSGQUEUE": RLIMIT_MSGQUEUE,
58            "NSOCK": VLIMIT_NSOCK,
59            "OPENFD": VLIMIT_OPENFD,
60            "ANON": VLIMIT_ANON,
61            "SHMEM": VLIMIT_SHMEM}
62
63 class NoSuchVServer(Exception): pass
64
65
66 class VServerConfig:
67     def __init__(self, name, directory):
68         self.name = name
69         self.dir = directory
70
71     def get(self, option, default = None):
72         try:
73             f = open(os.path.join(self.dir, option), "r")
74             buf = f.readline().rstrip()
75             f.close()
76             return buf
77         except KeyError, e:
78             # No mapping exists for this option
79             raise e
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         (child_stdin, child_stdout, child_stderr) = os.popen3(cmd)
424         child_stdin.close()
425         line = child_stdout.readline()
426         if not line:
427             sys.stderr.write(child_stderr.readline())
428         (space, inodes) = line.split()
429         self.disk_inodes = int(inodes)
430         self.disk_blocks = int(space)
431         #(self.disk_inodes, self.disk_blocks) = vduimpl.vdu(self.dir)
432
433         return self.disk_blocks * 1024
434
435     def stop(self, signal = signal.SIGKILL):
436         vserverimpl.killall(self.ctx, signal)
437         self.vm_running = False
438         self.rlimits_changed = False
439
440
441
442 def create(vm_name, static = False, ctor = VServer):
443
444     options = []
445     if static:
446         options += ['--static']
447     runcmd.run('vuseradd', options + [vm_name])
448     vm_id = pwd.getpwnam(vm_name)[2]
449
450     return ctor(vm_name, vm_id)