be76a503aeb92e4b793d6939d927202e968c406f
[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 __do_chroot(self):
181
182         os.chroot(self.dir)
183         os.chdir("/")
184
185     def chroot_call(self, fn, *args):
186
187         cwd_fd = os.open(".", os.O_RDONLY)
188         try:
189             root_fd = os.open("/", os.O_RDONLY)
190             try:
191                 self.__do_chroot()
192                 result = fn(*args)
193             finally:
194                 os.fchdir(root_fd)
195                 os.chroot(".")
196                 os.fchdir(cwd_fd)
197                 os.close(root_fd)
198         finally:
199             os.close(cwd_fd)
200         return result
201
202     def set_disklimit(self, block_limit):
203         # block_limit is in kB
204         if block_limit == 0:
205             try:
206                 vserverimpl.unsetdlimit(self.dir, self.ctx)
207             except OSError, e:
208                 print "Unexpected error with unsetdlimit for context %d" % self.ctx
209             return
210
211         if self.vm_running:
212             block_usage = vserverimpl.DLIMIT_KEEP
213             inode_usage = vserverimpl.DLIMIT_KEEP
214         else:
215             # init_disk_info() must have been called to get usage values
216             block_usage = self.disk_blocks
217             inode_usage = self.disk_inodes
218
219
220         try:
221             vserverimpl.setdlimit(self.dir,
222                                   self.ctx,
223                                   block_usage,
224                                   block_limit,
225                                   inode_usage,
226                                   vserverimpl.DLIMIT_INF,  # inode limit
227                                   2)   # %age reserved for root
228         except OSError, e:
229             print "Unexpected error with setdlimit for context %d" % self.ctx
230
231
232         self.config.update('dlimits/0/space_total', block_limit)
233
234     def is_running(self):
235         return vserverimpl.isrunning(self.ctx)
236     
237     def get_disklimit(self):
238
239         try:
240             (self.disk_blocks, block_limit, self.disk_inodes, inode_limit,
241              reserved) = vserverimpl.getdlimit(self.dir, self.ctx)
242         except OSError, ex:
243             if ex.errno != errno.ESRCH:
244                 raise
245             # get here if no vserver disk limit has been set for xid
246             block_limit = -1
247
248         return block_limit
249
250     def set_sched_config(self, cpu_share, sched_flags):
251
252         """ Write current CPU scheduler parameters to the vserver
253         configuration file. This method does not modify the kernel CPU
254         scheduling parameters for this context. """
255
256         if sched_flags & SCHED_CPU_GUARANTEED:
257             cpu_guaranteed = cpu_share
258         else:
259             cpu_guaranteed = 0
260         self.config.update('sched/fill-rate2', cpu_share)
261         self.config.update('sched/fill-rate', cpu_guaranteed)
262
263         if self.vm_running:
264             self.set_sched(cpu_share, sched_flags)
265
266     def set_sched(self, cpu_share, sched_flags = 0):
267         """ Update kernel CPU scheduling parameters for this context. """
268         vserverimpl.setsched(self.ctx, cpu_share, sched_flags)
269
270     def get_sched(self):
271         # have no way of querying scheduler right now on a per vserver basis
272         return (-1, False)
273
274     def set_bwlimit(self, minrate = bwlimit.bwmin, maxrate = None,
275                     exempt_min = None, exempt_max = None,
276                     share = None, dev = "eth0"):
277
278         if minrate is None:
279             bwlimit.off(self.ctx, dev)
280         else:
281             bwlimit.on(self.ctx, dev, share,
282                        minrate, maxrate, exempt_min, exempt_max)
283
284     def get_bwlimit(self, dev = "eth0"):
285
286         result = bwlimit.get(self.ctx)
287         # result of bwlimit.get is (ctx, share, minrate, maxrate)
288         if result:
289             result = result[1:]
290         return result
291
292     def open(self, filename, mode = "r", bufsize = -1):
293
294         return self.chroot_call(open, filename, mode, bufsize)
295
296     def __do_chcontext(self, state_file):
297
298         if state_file:
299             print >>state_file, "%u" % self.ctx
300             state_file.close()
301
302         if vserverimpl.chcontext(self.ctx):
303             self.set_resources()
304             vserverimpl.setup_done(self.ctx)
305
306     def __prep(self, runlevel, log):
307
308         """ Perform all the crap that the vserver script does before
309         actually executing the startup scripts. """
310
311         # remove /var/run and /var/lock/subsys files
312         # but don't remove utmp from the top-level /var/run
313         RUNDIR = "/var/run"
314         LOCKDIR = "/var/lock/subsys"
315         filter_fn = lambda fs: filter(lambda f: f != 'utmp', fs)
316         garbage = reduce((lambda (out, ff), (dir, subdirs, files):
317                           (out + map((dir + "/").__add__, ff(files)),
318                            lambda fs: fs)),
319                          list(os.walk(RUNDIR)),
320                          ([], filter_fn))[0]
321         garbage += filter(os.path.isfile, map((LOCKDIR + "/").__add__,
322                                               os.listdir(LOCKDIR)))
323         for f in garbage:
324             os.unlink(f)
325
326         # set the initial runlevel
327         f = open(RUNDIR + "/utmp", "w")
328         utmp.set_runlevel(f, runlevel)
329         f.close()
330
331         # mount /proc and /dev/pts
332         self.__do_mount("none", "/proc", "proc")
333         # XXX - magic mount options
334         self.__do_mount("none", "/dev/pts", "devpts", 0, "gid=5,mode=0620")
335
336     def __do_mount(self, *mount_args):
337
338         try:
339             mountimpl.mount(*mount_args)
340         except OSError, ex:
341             if ex.errno == errno.EBUSY:
342                 # assume already mounted
343                 return
344             raise ex
345
346     def enter(self):
347         self.__do_chroot()
348         self.__do_chcontext(None)
349
350     def start(self, wait, runlevel = 3):
351         self.vm_running = True
352         self.rlimits_changed = False
353
354         child_pid = os.fork()
355         if child_pid == 0:
356             # child process
357             try:
358                 # get a new session
359                 os.setsid()
360
361                 # open state file to record vserver info
362                 state_file = open("/var/run/vservers/%s" % self.name, "w")
363
364                 # use /dev/null for stdin, /var/log/boot.log for stdout/err
365                 os.close(0)
366                 os.close(1)
367                 os.open("/dev/null", os.O_RDONLY)
368                 self.__do_chroot()
369                 log = open("/var/log/boot.log", "w", 0)
370                 os.dup2(1, 2)
371
372                 print >>log, ("%s: starting the virtual server %s" %
373                               (time.asctime(time.gmtime()), self.name))
374
375                 # perform pre-init cleanup
376                 self.__prep(runlevel, log)
377
378                 # execute each init script in turn
379                 # XXX - we don't support all scripts that vserver script does
380                 self.__do_chcontext(state_file)
381                 for cmd in self.INITSCRIPTS + [None]:
382                         try:
383                             # enter vserver context
384                             arg_subst = { 'runlevel': runlevel }
385                             cmd_args = [cmd[0]] + map(lambda x: x % arg_subst,
386                                             cmd[1:])
387                             print >>log, "executing '%s'" % " ".join(cmd_args)
388                             os.spawnvp(os.P_WAIT,cmd[0],*cmd_args)
389                         except:
390                                 traceback.print_exc()
391                                 os._exit(1)
392
393             # we get here due to an exception in the top-level child process
394             except Exception, ex:
395                 traceback.print_exc()
396             os._exit(0)
397
398         # parent process
399         return child_pid
400
401     def set_resources(self):
402
403         """ Called when vserver context is entered for first time,
404         should be overridden by subclass. """
405
406         pass
407
408     def init_disk_info(self):
409         cmd = "/usr/sbin/vdu --script --space --inodes --blocksize 1024 --xid %d %s" % (self.ctx, self.dir)
410         (child_stdin, child_stdout, child_stderr) = os.popen3(cmd)
411         child_stdin.close()
412         line = child_stdout.readline()
413         if not line:
414             sys.stderr.write(child_stderr.readline())
415         (space, inodes) = line.split()
416         self.disk_inodes = int(inodes)
417         self.disk_blocks = int(space)
418         #(self.disk_inodes, self.disk_blocks) = vduimpl.vdu(self.dir)
419
420         return self.disk_blocks * 1024
421
422     def stop(self, signal = signal.SIGKILL):
423         vserverimpl.killall(self.ctx, signal)
424         self.vm_running = False
425         self.rlimits_changed = False
426
427
428
429 def create(vm_name, static = False, ctor = VServer):
430
431     options = []
432     if static:
433         options += ['--static']
434     runcmd.run('vuseradd', options + [vm_name])
435     vm_id = pwd.getpwnam(vm_name)[2]
436
437     return ctor(vm_name, vm_id)