6de72cb3edc9b9a5d37a774f81d81691be55871e
[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, vduimpl
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
50 class NoSuchVServer(Exception): pass
51
52
53 class VServer:
54
55     INITSCRIPTS = [('/etc/rc.vinit', 'start'),
56                    ('/etc/rc.d/rc', '%(runlevel)d')]
57
58     def __init__(self, name, vm_id = None, vm_running = False):
59
60         self.name = name
61         self.limits_changed = False
62         self.config_file = "/etc/vservers/%s.conf" % name
63         self.dir = "%s/%s" % (vserverimpl.VSERVER_BASEDIR, name)
64         if not (os.path.isdir(self.dir) and
65                 os.access(self.dir, os.R_OK | os.W_OK | os.X_OK)):
66             raise NoSuchVServer, "no such vserver: " + name
67         self.config = {}
68         for config_file in ["/etc/vservers.conf", self.config_file]:
69             try:
70                 self.config.update(self.__read_config_file(config_file))
71             except IOError, ex:
72                 if ex.errno != errno.ENOENT:
73                     raise
74         self.remove_caps = ~vserverimpl.CAP_SAFE;
75         if vm_id == None:
76             vm_id = int(self.config['S_CONTEXT'])
77         self.ctx = vm_id
78         self.vm_running = vm_running
79
80         # For every resource key listed in the limit table, add in a
81         # new method with which one can get/set the resource's hard,
82         # soft, minimum limits.
83         limits = {"CPU": RLIMIT_CPU,
84                   "RSS": RLIMIT_RSS,
85                   "NPROC": RLIMIT_NPROC,
86                   "NOFILE": RLIMIT_NOFILE,
87                   "MEMLOCK": RLIMIT_MEMLOCK,
88                   "AS": RLIMIT_AS,
89                   "LOCKS": RLIMIT_LOCKS,
90                   "SIGPENDING": RLIMIT_SIGPENDING,
91                   "MSGQUEUE": RLIMIT_MSGQUEUE,
92                   "NSOCK": VLIMIT_NSOCK,
93                   "OPENFD": VLIMIT_OPENFD,
94                   "ANON": VLIMIT_ANON,
95                   "SHMEM": VLIMIT_SHMEM}
96         for meth in limits.keys():
97             resource_type = limits[meth]
98             func = lambda \
99                        hard=VC_LIM_KEEP,\
100                        soft=VC_LIM_KEEP,\
101                        minimum=VC_LIM_KEEP:\
102                        self.__set_vserver_limit(resource_type,\
103                                                 hard, \
104                                                 soft,\
105                                                 minimum)
106             self.__dict__["set_%s_limit"%meth] = func
107
108             func = lambda \
109                        hard=VC_LIM_KEEP,\
110                        soft=VC_LIM_KEEP,\
111                        minimum=VC_LIM_KEEP:\
112                        self.__set_vserver_config(meth, resource_type, \
113                                                 hard, \
114                                                 soft,\
115                                                 minimum)
116             self.__dict__["set_%s_config"%meth] = func
117
118             func = lambda : self.__get_vserver_limit(resource_type)
119             self.__dict__["get_%s_limit"%meth] = func
120
121             func = lambda : self.__get_vserver_config(meth,resource_type)
122             self.__dict__["get_%s_config"%meth] = func
123     
124     def have_limits_changed(self):
125         return self.limits_changed
126
127     def __set_vserver_limit(self,resource_type,hard,soft,minimum):
128         """Generic set resource limit function for vserver"""
129         if self.is_running():
130             changed = False
131             old_hard, old_soft, old_minimum = self.__get_vserver_limit(resource_type)
132             if old_hard != VC_LIM_KEEP and old_hard <> hard: changed = True
133             if old_soft != VC_LIM_KEEP and old_soft <> soft: changed = True
134             if old_minimu != VC_LIM_KEEP and old_minimum <> minimum: changed = True
135             self.limits_changed = self.limits_changed or changed 
136             ret = vserverimpl.setrlimit(self.ctx,resource_type,hard,soft,minimum)
137
138     def __set_vserver_config(self,meth,resource_type,hard,soft,minimum):
139         """Generic set resource limit function for vserver"""
140         resources = {}
141         if hard <> VC_LIM_KEEP:
142             resources["VS_%s_HARD"%meth] = hard
143         if soft <> VC_LIM_KEEP:
144             resources["VS_%s_SOFT"%meth] = soft
145         if minimum <> VC_LIM_KEEP:
146             resources["VS_%s_MINIMUM"%meth] = minimum
147         if len(resources)>0:
148             self.update_resources(resources)
149         self.__set_vserver_limit(resource_type,hard,soft,minimum)
150
151     def __get_vserver_limit(self,resource_type):
152         """Generic get resource configuration function for vserver"""
153         if self.is_running():
154             ret = vserverimpl.getrlimit(self.ctx,resource_type)
155         else:
156             ret = __get_vserver_config(meth,resource_type)
157         return ret
158
159     def __get_vserver_config(self,meth,resource_type):
160         """Generic get resource configuration function for vserver"""
161         hard = int(self.config.get("VS_%s_HARD"%meth,VC_LIM_KEEP))
162         soft = int(self.config.get("VS_%s_SOFT"%meth,VC_LIM_KEEP))
163         minimum = int(self.config.get("VS_%s_MINIMUM"%meth,VC_LIM_KEEP))
164         return (hard,soft,minimum)
165
166     def set_WHITELISTED_config(self,whitelisted):
167         resources = {'VS_WHITELISTED': whitelisted}
168         self.update_resources(resources)
169
170     config_var_re = re.compile(r"^ *([A-Z_]+)=(.*)\n?$", re.MULTILINE)
171
172     def __read_config_file(self, filename):
173
174         f = open(filename, "r")
175         data = f.read()
176         f.close()
177         config = {}
178         for m in self.config_var_re.finditer(data):
179             (key, val) = m.groups()
180             config[key] = val.strip('"')
181         return config
182
183     def __update_config_file(self, filename, newvars):
184
185         # read old file, apply changes
186         f = open(filename, "r")
187         data = f.read()
188         f.close()
189         todo = newvars.copy()
190         changed = False
191         offset = 0
192         for m in self.config_var_re.finditer(data):
193             (key, val) = m.groups()
194             newval = todo.pop(key, None)
195             if newval != None:
196                 data = data[:offset+m.start(2)] + str(newval) + data[offset+m.end(2):]
197                 offset += len(str(newval)) - (m.end(2)-m.start(2))
198                 changed = True
199         for (newkey, newval) in todo.items():
200             data += "%s=%s\n" % (newkey, newval)
201             changed = True
202
203         if not changed:
204             return
205
206         # write new file
207         newfile = filename + ".new"
208         f = open(newfile, "w")
209         f.write(data)
210         f.close()
211
212         # replace old file with new
213         os.rename(newfile, filename)
214
215     def __do_chroot(self):
216
217         os.chroot(self.dir)
218         os.chdir("/")
219
220     def chroot_call(self, fn, *args):
221
222         cwd_fd = os.open(".", os.O_RDONLY)
223         try:
224             root_fd = os.open("/", os.O_RDONLY)
225             try:
226                 self.__do_chroot()
227                 result = fn(*args)
228             finally:
229                 os.fchdir(root_fd)
230                 os.chroot(".")
231                 os.fchdir(cwd_fd)
232                 os.close(root_fd)
233         finally:
234             os.close(cwd_fd)
235         return result
236
237     def set_disklimit(self, block_limit):
238         # block_limit is in kB
239         if block_limit == 0:
240             vserverimpl.unsetdlimit(self.dir, self.ctx)
241             return
242
243         if self.vm_running:
244             block_usage = vserverimpl.DLIMIT_KEEP
245             inode_usage = vserverimpl.DLIMIT_KEEP
246         else:
247             # init_disk_info() must have been called to get usage values
248             block_usage = self.disk_blocks
249             inode_usage = self.disk_inodes
250
251         vserverimpl.setdlimit(self.dir,
252                               self.ctx,
253                               block_usage,
254                               block_limit,
255                               inode_usage,
256                               vserverimpl.DLIMIT_INF,  # inode limit
257                               2)   # %age reserved for root
258
259         resources = {'VS_DISK_MAX': block_limit}
260         self.update_resources(resources)
261
262     def is_running(self):
263         return vserverimpl.isrunning(self.ctx)
264     
265     def get_disklimit(self):
266
267         try:
268             (self.disk_blocks, block_limit, self.disk_inodes, inode_limit,
269              reserved) = vserverimpl.getdlimit(self.dir, self.ctx)
270         except OSError, ex:
271             if ex.errno != errno.ESRCH:
272                 raise
273             # get here if no vserver disk limit has been set for xid
274             block_limit = -1
275
276         return block_limit
277
278     def set_sched_config(self, cpu_share, sched_flags):
279
280         """ Write current CPU scheduler parameters to the vserver
281         configuration file. This method does not modify the kernel CPU
282         scheduling parameters for this context. """
283
284         if cpu_share == int(self.config.get("CPULIMIT", -1)):
285             return
286         cpu_guaranteed = sched_flags & SCHED_CPU_GUARANTEED
287         cpu_config = { "CPULIMIT": cpu_share, "CPUGUARANTEED": cpu_guaranteed }
288         self.update_resources(cpu_config)
289         if self.vm_running:
290             self.set_sched(cpu_share, sched_flags)
291
292     def set_sched(self, cpu_share, sched_flags = 0):
293         """ Update kernel CPU scheduling parameters for this context. """
294         vserverimpl.setsched(self.ctx, cpu_share, sched_flags)
295
296     def get_sched(self):
297         # have no way of querying scheduler right now on a per vserver basis
298         return (-1, False)
299
300     def set_bwlimit(self, minrate = bwlimit.bwmin, maxrate = None,
301                     exempt_min = None, exempt_max = None,
302                     share = None, dev = "eth0"):
303
304         if minrate is None:
305             bwlimit.off(self.ctx, dev)
306         else:
307             bwlimit.on(self.ctx, dev, share,
308                        minrate, maxrate, exempt_min, exempt_max)
309
310     def get_bwlimit(self, dev = "eth0"):
311
312         result = bwlimit.get(self.ctx)
313         # result of bwlimit.get is (ctx, share, minrate, maxrate)
314         if result:
315             result = result[1:]
316         return result
317
318     def open(self, filename, mode = "r", bufsize = -1):
319
320         return self.chroot_call(open, filename, mode, bufsize)
321
322     def __do_chcontext(self, state_file):
323
324         if state_file:
325             print >>state_file, "S_CONTEXT=%u" % self.ctx
326             print >>state_file, "S_PROFILE="
327             state_file.close()
328
329         if vserverimpl.chcontext(self.ctx):
330             self.set_resources()
331             vserverimpl.setup_done(self.ctx)
332
333     def __prep(self, runlevel, log):
334
335         """ Perform all the crap that the vserver script does before
336         actually executing the startup scripts. """
337
338         # remove /var/run and /var/lock/subsys files
339         # but don't remove utmp from the top-level /var/run
340         RUNDIR = "/var/run"
341         LOCKDIR = "/var/lock/subsys"
342         filter_fn = lambda fs: filter(lambda f: f != 'utmp', fs)
343         garbage = reduce((lambda (out, ff), (dir, subdirs, files):
344                           (out + map((dir + "/").__add__, ff(files)),
345                            lambda fs: fs)),
346                          list(os.walk(RUNDIR)),
347                          ([], filter_fn))[0]
348         garbage += filter(os.path.isfile, map((LOCKDIR + "/").__add__,
349                                               os.listdir(LOCKDIR)))
350         for f in garbage:
351             os.unlink(f)
352
353         # set the initial runlevel
354         f = open(RUNDIR + "/utmp", "w")
355         utmp.set_runlevel(f, runlevel)
356         f.close()
357
358         # mount /proc and /dev/pts
359         self.__do_mount("none", "/proc", "proc")
360         # XXX - magic mount options
361         self.__do_mount("none", "/dev/pts", "devpts", 0, "gid=5,mode=0620")
362
363     def __do_mount(self, *mount_args):
364
365         try:
366             mountimpl.mount(*mount_args)
367         except OSError, ex:
368             if ex.errno == errno.EBUSY:
369                 # assume already mounted
370                 return
371             raise ex
372
373     def enter(self):
374
375         state_file = open("/var/run/vservers/%s.ctx" % self.name, "w")
376         self.__do_chroot()
377         self.__do_chcontext(state_file)
378
379     def start(self, wait, runlevel = 3):
380         self.vm_running = True
381         self.limits_changed = False
382
383         child_pid = os.fork()
384         if child_pid == 0:
385             # child process
386             try:
387                 # get a new session
388                 os.setsid()
389
390                 # open state file to record vserver info
391                 state_file = open("/var/run/vservers/%s.ctx" % self.name, "w")
392
393                 # use /dev/null for stdin, /var/log/boot.log for stdout/err
394                 os.close(0)
395                 os.close(1)
396                 os.open("/dev/null", os.O_RDONLY)
397                 self.__do_chroot()
398                 log = open("/var/log/boot.log", "w", 0)
399                 os.dup2(1, 2)
400
401                 print >>log, ("%s: starting the virtual server %s" %
402                               (time.asctime(time.gmtime()), self.name))
403
404                 # perform pre-init cleanup
405                 self.__prep(runlevel, log)
406
407                 # execute each init script in turn
408                 # XXX - we don't support all scripts that vserver script does
409                 cmd_pid = 0
410                 first_child = True
411                 for cmd in self.INITSCRIPTS + [None]:
412                     # wait for previous command to terminate, unless it
413                     # is the last one and the caller has specified to wait
414                     if cmd_pid and (cmd != None or wait):
415                         try:
416                             os.waitpid(cmd_pid, 0)
417                         except:
418                             print >>log, "error waiting for %s:" % cmd_pid
419                             traceback.print_exc()
420
421                     # end of list
422                     if cmd == None:
423                         os._exit(0)
424
425                     # fork and exec next command
426                     cmd_pid = os.fork()
427                     if cmd_pid == 0:
428                         try:
429                             # enter vserver context
430                             self.__do_chcontext(state_file)
431                             arg_subst = { 'runlevel': runlevel }
432                             cmd_args = [cmd[0]] + map(lambda x: x % arg_subst,
433                                                       cmd[1:])
434                             print >>log, "executing '%s'" % " ".join(cmd_args)
435                             os.execl(cmd[0], *cmd_args)
436                         except:
437                             traceback.print_exc()
438                             os._exit(1)
439                     else:
440                         # don't want to write state_file multiple times
441                         state_file = None
442
443             # we get here due to an exception in the top-level child process
444             except Exception, ex:
445                 traceback.print_exc()
446             os._exit(0)
447
448         # parent process
449         return child_pid
450
451     def set_resources(self):
452
453         """ Called when vserver context is entered for first time,
454         should be overridden by subclass. """
455
456         pass
457
458     def update_resources(self, resources):
459
460         self.config.update(resources)
461
462         # write new values to configuration file
463         self.__update_config_file(self.config_file, resources)
464
465     def init_disk_info(self):
466
467         (self.disk_inodes, self.disk_blocks, size) = vduimpl.vdu(self.dir)
468
469         return size
470
471     def stop(self, signal = signal.SIGKILL):
472         vserverimpl.killall(self.ctx, signal)
473         self.vm_running = False
474         self.limits_changed = False
475
476
477
478 def create(vm_name, static = False, ctor = VServer):
479
480     options = []
481     if static:
482         options += ['--static']
483     runcmd.run('vuseradd', options + [vm_name])
484     vm_id = pwd.getpwnam(vm_name)[2]
485
486     return ctor(vm_name, vm_id)