merge with 0.30.213
[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     mapping = {
68         'S_CONTEXT':            'context',
69         'VS_WHITELISTED':       'whitelisted',
70         'CPULIMIT':             'sched/fill-rate2',
71         'CPUGUARANTEED':        'sched/fill-rate',
72         'VS_DISK_MAX':          'dlimits/0/space_total',
73       }
74
75     def __init__(self, name, directory):
76         global RLIMITS
77         for i in RLIMITS.keys():
78             for j in ('HARD', 'SOFT', 'MINIMUM'):
79                 self.mapping['VS_%s_%s' % (i, j)] = 'rlimits/%s.%s' % (i.lower(), j.replace('MINIMUM', 'min').lower())
80         self.name = name
81         self.dir = directory
82
83     def get(self, option, default = None):
84         try:
85             filename = self.mapping[option]
86             f = open(os.path.join(self.dir, filename), "r")
87             buf = f.readline().rstrip()
88             f.close()
89             return buf
90         except KeyError, e:
91             # No mapping exists for this option
92             raise e
93         except IOError, e:
94             if default is not None:
95                 return default
96             else:
97                 raise KeyError, "Key %s is not set for %s" % (option, self.name)
98
99     def update(self, vars):
100         for (option, value) in vars.iteritems():
101             try:
102                 old_umask = os.umask(0022)
103                 filename = os.path.join(self.dir, self.mapping[option])
104                 try:
105                     os.makedirs(os.path.dirname(filename), 0755)
106                 except:
107                     pass
108                 f = open(filename, 'w')
109                 f.write("%s\n" % value)
110                 f.close()
111                 os.umask(old_umask)
112             except KeyError, e:
113                 raise KeyError, "Don't know how to handle %s, sorry" % option
114
115
116 class VServer:
117
118     INITSCRIPTS = [('/etc/rc.vinit', 'start'),
119                    ('/etc/rc.d/rc', '%(runlevel)d')]
120
121     def __init__(self, name, vm_id = None, vm_running = False):
122
123         self.name = name
124         self.rlimits_changed = False
125         self.dir = "%s/%s" % (vserverimpl.VSERVER_BASEDIR, name)
126         if not (os.path.isdir(self.dir) and
127                 os.access(self.dir, os.R_OK | os.W_OK | os.X_OK)):
128             raise NoSuchVServer, "no such vserver: " + name
129         self.config = VServerConfig(name, "/etc/vservers/%s" % name)
130         self.remove_caps = ~vserverimpl.CAP_SAFE;
131         if vm_id == None:
132             vm_id = int(self.config.get('S_CONTEXT'))
133         self.ctx = vm_id
134         self.vm_running = vm_running
135
136     def have_limits_changed(self):
137         return self.rlimits_changed
138
139     def set_rlimit_limit(self,type,hard,soft,minimum):
140         """Generic set resource limit function for vserver"""
141         global RLIMITS
142         changed = False
143         try:
144             old_hard, old_soft, old_minimum = self.get_rlimit_limit(type)
145             if old_hard != VC_LIM_KEEP and old_hard <> hard: changed = True
146             if old_soft != VC_LIM_KEEP and old_soft <> soft: changed = True
147             if old_minimum != VC_LIM_KEEP and old_minimum <> minimum: changed = True
148             self.rlimits_changed = self.rlimits_changed or changed 
149         except OSError, e:
150             if self.is_running(): print "Unexpected error with getrlimit for running context %d" % self.ctx
151
152         resource_type = RLIMITS[type]
153         try:
154             ret = vserverimpl.setrlimit(self.ctx,resource_type,hard,soft,minimum)
155         except OSError, e:
156             if self.is_running(): print "Unexpected error with setrlimit for running context %d" % self.ctx
157
158     def set_rlimit_config(self,type,hard,soft,minimum):
159         """Generic set resource limit function for vserver"""
160         resources = {}
161         if hard <> VC_LIM_KEEP:
162             resources["VS_%s_HARD"%type] = hard
163         if soft <> VC_LIM_KEEP:
164             resources["VS_%s_SOFT"%type] = soft
165         if minimum <> VC_LIM_KEEP:
166             resources["VS_%s_MINIMUM"%type] = minimum
167         if len(resources)>0:
168             self.update_resources(resources)
169         self.set_rlimit_limit(type,hard,soft,minimum)
170
171     def get_rlimit_limit(self,type):
172         """Generic get resource configuration function for vserver"""
173         global RLIMITS
174         resource_type = RLIMITS[type]
175         try:
176             ret = vserverimpl.getrlimit(self.ctx,resource_type)
177         except OSError, e:
178             print "Unexpected error with getrlimit for context %d" % self.ctx
179             ret = self.get_rlimit_config(type)
180         return ret
181
182     def get_rlimit_config(self,type):
183         """Generic get resource configuration function for vserver"""
184         hard = int(self.config.get("VS_%s_HARD"%type,VC_LIM_KEEP))
185         soft = int(self.config.get("VS_%s_SOFT"%type,VC_LIM_KEEP))
186         minimum = int(self.config.get("VS_%s_MINIMUM"%type,VC_LIM_KEEP))
187         return (hard,soft,minimum)
188
189     def set_WHITELISTED_config(self,whitelisted):
190         resources = {'VS_WHITELISTED': whitelisted}
191         self.update_resources(resources)
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         resources = {'VS_DISK_MAX': block_limit}
246         self.update_resources(resources)
247
248     def is_running(self):
249         return vserverimpl.isrunning(self.ctx)
250     
251     def get_disklimit(self):
252
253         try:
254             (self.disk_blocks, block_limit, self.disk_inodes, inode_limit,
255              reserved) = vserverimpl.getdlimit(self.dir, self.ctx)
256         except OSError, ex:
257             if ex.errno != errno.ESRCH:
258                 raise
259             # get here if no vserver disk limit has been set for xid
260             block_limit = -1
261
262         return block_limit
263
264     def set_sched_config(self, cpu_share, sched_flags):
265
266         """ Write current CPU scheduler parameters to the vserver
267         configuration file. This method does not modify the kernel CPU
268         scheduling parameters for this context. """
269
270         if cpu_share == int(self.config.get("CPULIMIT", -1)):
271             return
272         cpu_guaranteed = sched_flags & SCHED_CPU_GUARANTEED
273         cpu_config = { "CPULIMIT": cpu_share, "CPUGUARANTEED": cpu_guaranteed }
274         self.update_resources(cpu_config)
275         if self.vm_running:
276             self.set_sched(cpu_share, sched_flags)
277
278     def set_sched(self, cpu_share, sched_flags = 0):
279         """ Update kernel CPU scheduling parameters for this context. """
280         vserverimpl.setsched(self.ctx, cpu_share, sched_flags)
281
282     def get_sched(self):
283         # have no way of querying scheduler right now on a per vserver basis
284         return (-1, False)
285
286     def set_bwlimit(self, minrate = bwlimit.bwmin, maxrate = None,
287                     exempt_min = None, exempt_max = None,
288                     share = None, dev = "eth0"):
289
290         if minrate is None:
291             bwlimit.off(self.ctx, dev)
292         else:
293             bwlimit.on(self.ctx, dev, share,
294                        minrate, maxrate, exempt_min, exempt_max)
295
296     def get_bwlimit(self, dev = "eth0"):
297
298         result = bwlimit.get(self.ctx)
299         # result of bwlimit.get is (ctx, share, minrate, maxrate)
300         if result:
301             result = result[1:]
302         return result
303
304     def open(self, filename, mode = "r", bufsize = -1):
305
306         return self.chroot_call(open, filename, mode, bufsize)
307
308     def __do_chcontext(self, state_file):
309
310         if state_file:
311             print >>state_file, "%u" % self.ctx
312             state_file.close()
313
314         if vserverimpl.chcontext(self.ctx):
315             self.set_resources()
316             vserverimpl.setup_done(self.ctx)
317
318     def __prep(self, runlevel, log):
319
320         """ Perform all the crap that the vserver script does before
321         actually executing the startup scripts. """
322
323         # remove /var/run and /var/lock/subsys files
324         # but don't remove utmp from the top-level /var/run
325         RUNDIR = "/var/run"
326         LOCKDIR = "/var/lock/subsys"
327         filter_fn = lambda fs: filter(lambda f: f != 'utmp', fs)
328         garbage = reduce((lambda (out, ff), (dir, subdirs, files):
329                           (out + map((dir + "/").__add__, ff(files)),
330                            lambda fs: fs)),
331                          list(os.walk(RUNDIR)),
332                          ([], filter_fn))[0]
333         garbage += filter(os.path.isfile, map((LOCKDIR + "/").__add__,
334                                               os.listdir(LOCKDIR)))
335         for f in garbage:
336             os.unlink(f)
337
338         # set the initial runlevel
339         f = open(RUNDIR + "/utmp", "w")
340         utmp.set_runlevel(f, runlevel)
341         f.close()
342
343         # mount /proc and /dev/pts
344         self.__do_mount("none", "/proc", "proc")
345         # XXX - magic mount options
346         self.__do_mount("none", "/dev/pts", "devpts", 0, "gid=5,mode=0620")
347
348     def __do_mount(self, *mount_args):
349
350         try:
351             mountimpl.mount(*mount_args)
352         except OSError, ex:
353             if ex.errno == errno.EBUSY:
354                 # assume already mounted
355                 return
356             raise ex
357
358     def enter(self):
359         self.__do_chroot()
360         self.__do_chcontext(None)
361
362     def start(self, wait, runlevel = 3):
363         self.vm_running = True
364         self.rlimits_changed = False
365
366         child_pid = os.fork()
367         if child_pid == 0:
368             # child process
369             try:
370                 # get a new session
371                 os.setsid()
372
373                 # open state file to record vserver info
374                 state_file = open("/var/run/vservers/%s" % self.name, "w")
375
376                 # use /dev/null for stdin, /var/log/boot.log for stdout/err
377                 os.close(0)
378                 os.close(1)
379                 os.open("/dev/null", os.O_RDONLY)
380                 self.__do_chroot()
381                 log = open("/var/log/boot.log", "w", 0)
382                 os.dup2(1, 2)
383
384                 print >>log, ("%s: starting the virtual server %s" %
385                               (time.asctime(time.gmtime()), self.name))
386
387                 # perform pre-init cleanup
388                 self.__prep(runlevel, log)
389
390                 # execute each init script in turn
391                 # XXX - we don't support all scripts that vserver script does
392                 self.__do_chcontext(state_file)
393                 for cmd in self.INITSCRIPTS + [None]:
394                         try:
395                             # enter vserver context
396                             arg_subst = { 'runlevel': runlevel }
397                             cmd_args = [cmd[0]] + map(lambda x: x % arg_subst,
398                                             cmd[1:])
399                             print >>log, "executing '%s'" % " ".join(cmd_args)
400                             os.spawnvp(os.P_WAIT,cmd[0],*cmd_args)
401                         except:
402                                 traceback.print_exc()
403                                 os._exit(1)
404
405             # we get here due to an exception in the top-level child process
406             except Exception, ex:
407                 traceback.print_exc()
408             os._exit(0)
409
410         # parent process
411         return child_pid
412
413     def set_resources(self):
414
415         """ Called when vserver context is entered for first time,
416         should be overridden by subclass. """
417
418         pass
419
420     def update_resources(self, resources):
421
422         self.config.update(resources)
423
424     def init_disk_info(self):
425         cmd = "/usr/sbin/vdu --script --space --inodes --blocksize 1024 --xid %d %s" % (self.ctx, self.dir)
426         (child_stdin, child_stdout, child_stderr) = os.popen3(cmd)
427         child_stdin.close()
428         line = child_stdout.readline()
429         if not line:
430             sys.stderr.write(child_stderr.readline())
431         (space, inodes) = line.split()
432         self.disk_inodes = int(inodes)
433         self.disk_blocks = int(space)
434         #(self.disk_inodes, self.disk_blocks) = vduimpl.vdu(self.dir)
435
436         return self.disk_blocks * 1024
437
438     def stop(self, signal = signal.SIGKILL):
439         vserverimpl.killall(self.ctx, signal)
440         self.vm_running = False
441         self.rlimits_changed = False
442
443
444
445 def create(vm_name, static = False, ctor = VServer):
446
447     options = []
448     if static:
449         options += ['--static']
450     runcmd.run('vuseradd', options + [vm_name])
451     vm_id = pwd.getpwnam(vm_name)[2]
452
453     return ctor(vm_name, vm_id)