e27f6bb1a59545a0a25017b597c32310a09af3f3
[util-vserver.git] / python / vserver.py
1 # Copyright 2005 Princeton University
2
3 #$Id: vserver.py,v 1.65 2007/07/31 14:36:19 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                 try:
409                     os.close(0)
410                     os.close(1)
411                 except:
412                     pass
413                 fd = os.open("/dev/null", os.O_RDONLY)
414                 if fd != 0:
415                     os.dup2(fd, 0)
416                     os.close(fd)
417                 self.__do_chroot()
418                 log = open("/var/log/boot.log", "w", 0)
419                 if log.fileno() != 1:
420                     os.dup2(log.fileno(), 1)
421                 os.dup2(1, 2)
422
423                 print >>log, ("%s: starting the virtual server %s" %
424                               (time.asctime(time.gmtime()), self.name))
425
426                 # perform pre-init cleanup
427                 self.__prep(runlevel, log)
428
429                 # execute each init script in turn
430                 # XXX - we don't support all scripts that vserver script does
431                 self.__do_chcontext(state_file)
432                 for cmd in self.INITSCRIPTS + [None]:
433                      try:
434                          # enter vserver context
435                          arg_subst = { 'runlevel': runlevel }
436                          cmd_args = [cmd[0]] + map(lambda x: x % arg_subst,
437                                                    cmd[1:])
438                          print >>log, "executing '%s'" % " ".join(cmd_args)
439                          os.spawnvp(os.P_WAIT,cmd[0],*cmd_args)
440                      except:
441                          traceback.print_exc()
442                          os._exit(1)
443
444             # we get here due to an exception in the top-level child process
445             except Exception, ex:
446                 traceback.print_exc()
447             os._exit(0)
448
449         # parent process
450         return child_pid
451
452     def set_resources(self):
453
454         """ Called when vserver context is entered for first time,
455         should be overridden by subclass. """
456
457         pass
458
459     def init_disk_info(self):
460         cmd = "/usr/sbin/vdu --script --space --inodes --blocksize 1024 --xid %d %s" % (self.ctx, self.dir)
461         p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
462                              stdout=subprocess.PIPE, stderr=subprocess.PIPE,
463                              close_fds=True)
464         p.stdin.close()
465         line = p.stdout.readline()
466         if not line:
467             sys.stderr.write(p.stderr.read())
468         p.stdout.close()
469         p.stderr.close()
470         ret = p.wait()
471
472         (space, inodes) = line.split()
473         self.disk_inodes = int(inodes)
474         self.disk_blocks = int(space)
475         #(self.disk_inodes, self.disk_blocks) = vduimpl.vdu(self.dir)
476
477         return self.disk_blocks * 1024
478
479     def stop(self, signal = signal.SIGKILL):
480         vserverimpl.killall(self.ctx, signal)
481         self.vm_running = False
482         self.rlimits_changed = False
483
484
485
486 def create(vm_name, static = False, ctor = VServer):
487
488     options = []
489     if static:
490         options += ['--static']
491     runcmd.run('vuseradd', options + [vm_name])
492     vm_id = pwd.getpwnam(vm_name)[2]
493
494     return ctor(vm_name, vm_id)