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