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