32f27adc0dbadf76fa4ccbdba801fc3fadb43a6f
[util-vserver.git] / python / vserver.py
1 # Copyright 2005 Princeton University
2
3 #$Id: vserver.py,v 1.66 2007/07/31 16:31:04 dhozac Exp $
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 import subprocess
15 import resource
16
17 import mountimpl
18 import runcmd
19 import utmp
20 import vserverimpl
21 import cpulimit, bwlimit
22
23 from vserverimpl import VS_SCHED_CPU_GUARANTEED as SCHED_CPU_GUARANTEED
24 from vserverimpl import DLIMIT_INF
25 from vserverimpl import VC_LIM_KEEP
26 from vserverimpl import VLIMIT_NSOCK
27 from vserverimpl import VLIMIT_OPENFD
28 from vserverimpl import VLIMIT_ANON
29 from vserverimpl import VLIMIT_SHMEM
30
31 #
32 # these are the flags taken from the kernel linux/vserver/legacy.h
33 #
34 FLAGS_LOCK = 1
35 FLAGS_SCHED = 2  # XXX - defined in util-vserver/src/chcontext.c
36 FLAGS_NPROC = 4
37 FLAGS_PRIVATE = 8
38 FLAGS_INIT = 16
39 FLAGS_HIDEINFO = 32
40 FLAGS_ULIMIT = 64
41 FLAGS_NAMESPACE = 128
42
43 RLIMITS = { "NSOCK": VLIMIT_NSOCK,
44             "OPENFD": VLIMIT_OPENFD,
45             "ANON": VLIMIT_ANON,
46             "SHMEM": VLIMIT_SHMEM}
47
48 # add in the platform supported rlimits
49 for entry in resource.__dict__.keys():
50     if entry.find("RLIMIT_")==0:
51         k = entry[len("RLIMIT_"):]
52         if not RLIMITS.has_key(k):
53             RLIMITS[k]=resource.__dict__[entry]
54         else:
55             print "WARNING: duplicate RLIMITS key %s" % k
56
57 class NoSuchVServer(Exception): pass
58
59
60 class VServerConfig:
61     def __init__(self, name, directory):
62         self.name = name
63         self.dir = directory
64
65     def get(self, option, default = None):
66         try:
67             f = open(os.path.join(self.dir, option), "r")
68             buf = f.readline().rstrip()
69             f.close()
70             return buf
71         except IOError, e:
72             if default is not None:
73                 return default
74             else:
75                 raise KeyError, "Key %s is not set for %s" % (option, self.name)
76
77     def update(self, option, value):
78         try:
79             old_umask = os.umask(0022)
80             filename = os.path.join(self.dir, option)
81             try:
82                 os.makedirs(os.path.dirname(filename), 0755)
83             except:
84                 pass
85             f = open(filename, 'w')
86             if isinstance(value, list):
87                 f.write("%s\n" % "\n".join(value))
88             else:
89                 f.write("%s\n" % value)
90             f.close()
91             os.umask(old_umask)
92         except:
93             raise
94
95     def unset(self, option):
96         try:
97             filename = os.path.join(self.dir, option)
98             os.unlink(filename)
99             os.removedirs(os.path.dirname(filename))
100             return True
101         except:
102             return False
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 set_ipaddresses(self, addresses):
194         vserverimpl.netremove(self.ctx, "all")
195         for a in addresses.split(","):
196             vserverimpl.netadd(self.ctx, a)
197
198     def set_ipaddresses_config(self, addresses):
199         i = 0
200         for a in addresses.split(","):
201             self.config.update("interfaces/%d/ip" % i, a)
202             i += 1
203         while self.config.unset("interfaces/%d/ip" % i):
204             i += 1
205         self.set_ipaddresses(addresses)
206
207     def get_ipaddresses_config(self):
208         i = 0
209         ret = []
210         while True:
211             r = self.config.get("interfaces/%d/ip" % i, '')
212             if r == '':
213                 break
214             ret += [r]
215             i += 1
216         return ",".join(ret)
217
218     def get_ipaddresses(self):
219         # No clean way to do this right now.
220         return None
221
222     def __do_chroot(self):
223
224         os.chroot(self.dir)
225         os.chdir("/")
226
227     def chroot_call(self, fn, *args):
228
229         cwd_fd = os.open(".", os.O_RDONLY)
230         try:
231             root_fd = os.open("/", os.O_RDONLY)
232             try:
233                 self.__do_chroot()
234                 result = fn(*args)
235             finally:
236                 os.fchdir(root_fd)
237                 os.chroot(".")
238                 os.fchdir(cwd_fd)
239                 os.close(root_fd)
240         finally:
241             os.close(cwd_fd)
242         return result
243
244     def set_disklimit(self, block_limit):
245         # block_limit is in kB
246         if block_limit == 0:
247             try:
248                 vserverimpl.unsetdlimit(self.dir, self.ctx)
249             except OSError, e:
250                 print "Unexpected error with unsetdlimit for context %d" % self.ctx
251             return
252
253         if self.vm_running:
254             block_usage = vserverimpl.DLIMIT_KEEP
255             inode_usage = vserverimpl.DLIMIT_KEEP
256         else:
257             # init_disk_info() must have been called to get usage values
258             block_usage = self.disk_blocks
259             inode_usage = self.disk_inodes
260
261
262         try:
263             vserverimpl.setdlimit(self.dir,
264                                   self.ctx,
265                                   block_usage,
266                                   block_limit,
267                                   inode_usage,
268                                   vserverimpl.DLIMIT_INF,  # inode limit
269                                   2)   # %age reserved for root
270         except OSError, e:
271             print "Unexpected error with setdlimit for context %d" % self.ctx
272
273
274         self.config.update('dlimits/0/space_total', block_limit)
275
276     def is_running(self):
277         return vserverimpl.isrunning(self.ctx)
278     
279     def get_disklimit(self):
280
281         try:
282             (self.disk_blocks, block_limit, self.disk_inodes, inode_limit,
283              reserved) = vserverimpl.getdlimit(self.dir, self.ctx)
284         except OSError, ex:
285             if ex.errno != errno.ESRCH:
286                 raise
287             # get here if no vserver disk limit has been set for xid
288             block_limit = -1
289
290         return block_limit
291
292     def set_sched_config(self, cpu_share, sched_flags):
293
294         """ Write current CPU scheduler parameters to the vserver
295         configuration file. This method does not modify the kernel CPU
296         scheduling parameters for this context. """
297
298         if sched_flags & SCHED_CPU_GUARANTEED:
299             cpu_guaranteed = cpu_share
300         else:
301             cpu_guaranteed = 0
302         self.config.update('sched/fill-rate2', cpu_share)
303         self.config.update('sched/fill-rate', cpu_guaranteed)
304
305         if self.vm_running:
306             self.set_sched(cpu_share, sched_flags)
307
308     def set_sched(self, cpu_share, sched_flags = 0):
309         """ Update kernel CPU scheduling parameters for this context. """
310         vserverimpl.setsched(self.ctx, cpu_share, sched_flags)
311
312     def get_sched(self):
313         # have no way of querying scheduler right now on a per vserver basis
314         return (-1, False)
315
316     def set_bwlimit(self, minrate = bwlimit.bwmin, maxrate = None,
317                     exempt_min = None, exempt_max = None,
318                     share = None, dev = "eth0"):
319
320         if minrate is None:
321             bwlimit.off(self.ctx, dev)
322         else:
323             bwlimit.on(self.ctx, dev, share,
324                        minrate, maxrate, exempt_min, exempt_max)
325
326     def get_bwlimit(self, dev = "eth0"):
327
328         result = bwlimit.get(self.ctx)
329         # result of bwlimit.get is (ctx, share, minrate, maxrate)
330         if result:
331             result = result[1:]
332         return result
333
334     def open(self, filename, mode = "r", bufsize = -1):
335
336         return self.chroot_call(open, filename, mode, bufsize)
337
338     def __do_chcontext(self, state_file):
339
340         if state_file:
341             print >>state_file, "%u" % self.ctx
342             state_file.close()
343
344         if vserverimpl.chcontext(self.ctx, vserverimpl.text2bcaps(self.get_capabilities_config())):
345             self.set_resources()
346             vserverimpl.setup_done(self.ctx)
347
348     def __prep(self, runlevel, log):
349
350         """ Perform all the crap that the vserver script does before
351         actually executing the startup scripts. """
352
353         # remove /var/run and /var/lock/subsys files
354         # but don't remove utmp from the top-level /var/run
355         RUNDIR = "/var/run"
356         LOCKDIR = "/var/lock/subsys"
357         filter_fn = lambda fs: filter(lambda f: f != 'utmp', fs)
358         garbage = reduce((lambda (out, ff), (dir, subdirs, files):
359                           (out + map((dir + "/").__add__, ff(files)),
360                            lambda fs: fs)),
361                          list(os.walk(RUNDIR)),
362                          ([], filter_fn))[0]
363         garbage += filter(os.path.isfile, map((LOCKDIR + "/").__add__,
364                                               os.listdir(LOCKDIR)))
365         if False:
366             for f in garbage:
367                 os.unlink(f)
368
369         # set the initial runlevel
370         f = open(RUNDIR + "/utmp", "w")
371         utmp.set_runlevel(f, runlevel)
372         f.close()
373
374         # mount /proc and /dev/pts
375         self.__do_mount("none", "/proc", "proc")
376         # XXX - magic mount options
377         self.__do_mount("none", "/dev/pts", "devpts", 0, "gid=5,mode=0620")
378
379     def __do_mount(self, *mount_args):
380
381         try:
382             mountimpl.mount(*mount_args)
383         except OSError, ex:
384             if ex.errno == errno.EBUSY:
385                 # assume already mounted
386                 return
387             raise ex
388
389     def enter(self):
390         self.__do_chroot()
391         self.__do_chcontext(None)
392
393     def start(self, wait, runlevel = 3):
394         self.vm_running = True
395         self.rlimits_changed = False
396
397         child_pid = os.fork()
398         if child_pid == 0:
399             # child process
400             try:
401                 # get a new session
402                 os.setsid()
403
404                 # open state file to record vserver info
405                 state_file = open("/var/run/vservers/%s" % self.name, "w")
406
407                 # use /dev/null for stdin, /var/log/boot.log for stdout/err
408                 fd = os.open("/dev/null", os.O_RDONLY)
409                 if fd != 0:
410                     os.dup2(fd, 0)
411                     os.close(fd)
412                 self.__do_chroot()
413                 log = open("/var/log/boot.log", "w", 0)
414                 if log.fileno() != 1:
415                     os.dup2(log.fileno(), 1)
416                 os.dup2(1, 2)
417
418                 print >>log, ("%s: starting the virtual server %s" %
419                               (time.asctime(time.gmtime()), self.name))
420
421                 # perform pre-init cleanup
422                 self.__prep(runlevel, log)
423
424                 # execute each init script in turn
425                 # XXX - we don't support all scripts that vserver script does
426                 self.__do_chcontext(state_file)
427                 for cmd in self.INITSCRIPTS + [None]:
428                      try:
429                          # enter vserver context
430                          arg_subst = { 'runlevel': runlevel }
431                          cmd_args = [cmd[0]] + map(lambda x: x % arg_subst,
432                                                    cmd[1:])
433                          print >>log, "executing '%s'" % " ".join(cmd_args)
434                          os.spawnvp(os.P_WAIT,cmd[0],*cmd_args)
435                      except:
436                          traceback.print_exc()
437                          os._exit(1)
438
439             # we get here due to an exception in the top-level child process
440             except Exception, ex:
441                 traceback.print_exc()
442             os._exit(0)
443
444         # parent process
445         return child_pid
446
447     def set_resources(self):
448
449         """ Called when vserver context is entered for first time,
450         should be overridden by subclass. """
451
452         pass
453
454     def init_disk_info(self):
455         cmd = "/usr/sbin/vdu --script --space --inodes --blocksize 1024 --xid %d %s" % (self.ctx, self.dir)
456         p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
457                              stdout=subprocess.PIPE, stderr=subprocess.PIPE,
458                              close_fds=True)
459         p.stdin.close()
460         line = p.stdout.readline()
461         if not line:
462             sys.stderr.write(p.stderr.read())
463         p.stdout.close()
464         p.stderr.close()
465         ret = p.wait()
466
467         (space, inodes) = line.split()
468         self.disk_inodes = int(inodes)
469         self.disk_blocks = int(space)
470         #(self.disk_inodes, self.disk_blocks) = vduimpl.vdu(self.dir)
471
472         return self.disk_blocks * 1024
473
474     def stop(self, signal = signal.SIGKILL):
475         vserverimpl.killall(self.ctx, signal)
476         self.vm_running = False
477         self.rlimits_changed = False
478
479
480
481 def create(vm_name, static = False, ctor = VServer):
482
483     options = []
484     if static:
485         options += ['--static']
486     runcmd.run('vuseradd', options + [vm_name])
487     vm_id = pwd.getpwnam(vm_name)[2]
488
489     return ctor(vm_name, vm_id)