setrunlevel takes a filename and a runlevel.
[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         vserverimpl.setrunlevel(RUNDIR + "/utmp", runlevel)
395
396         # mount /proc and /dev/pts
397         self.__do_mount("none", self.dir, "/proc", "proc")
398         # XXX - magic mount options
399         self.__do_mount("none", self.dir, "/dev/pts", "devpts", 0, "gid=5,mode=0620")
400
401     def __do_mount(self, *mount_args):
402
403         try:
404             vserverimpl.mount(*mount_args)
405         except OSError, ex:
406             if ex.errno == errno.EBUSY:
407                 # assume already mounted
408                 return
409             raise ex
410
411     def enter(self):
412         self.config.cache_it()
413         self.__do_chroot()
414         self.__do_chcontext(None)
415
416     def start(self, wait, runlevel = 3):
417         self.vm_running = True
418         self.rlimits_changed = False
419
420         child_pid = os.fork()
421         if child_pid == 0:
422             # child process
423             try:
424                 # get a new session
425                 os.setsid()
426
427                 # open state file to record vserver info
428                 state_file = open("/var/run/vservers/%s" % self.name, "w")
429
430                 # use /dev/null for stdin, /var/log/boot.log for stdout/err
431                 fd = os.open("/dev/null", os.O_RDONLY)
432                 if fd != 0:
433                     os.dup2(fd, 0)
434                     os.close(fd)
435                 self.config.cache_it()
436                 self.__do_chroot()
437                 log = open("/var/log/boot.log", "w", 0)
438                 if log.fileno() != 1:
439                     os.dup2(log.fileno(), 1)
440                 os.dup2(1, 2)
441
442                 print >>log, ("%s: starting the virtual server %s" %
443                               (time.asctime(time.gmtime()), self.name))
444
445                 # perform pre-init cleanup
446                 self.__prep(runlevel, log)
447
448                 # execute each init script in turn
449                 # XXX - we don't support all scripts that vserver script does
450                 self.__do_chcontext(state_file)
451                 for cmd in self.INITSCRIPTS:
452                      try:
453                          # enter vserver context
454                          arg_subst = { 'runlevel': runlevel }
455                          cmd_args = [cmd[0]] + map(lambda x: x % arg_subst,
456                                                    cmd[1:])
457                          print >>log, "executing '%s'" % " ".join(cmd_args)
458                          os.spawnvp(os.P_NOWAIT,cmd[0],cmd_args)
459                      except:
460                          traceback.print_exc()
461                          os._exit(1)
462
463             # we get here due to an exception in the top-level child process
464             except Exception, ex:
465                 traceback.print_exc()
466             os._exit(0)
467
468         # parent process
469         return child_pid
470
471     def set_resources(self):
472
473         """ Called when vserver context is entered for first time,
474         should be overridden by subclass. """
475
476         pass
477
478     def init_disk_info(self):
479         cmd = "/usr/sbin/vdu --script --space --inodes --blocksize 1024 --xid %d %s" % (self.ctx, self.dir)
480         p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
481                              stdout=subprocess.PIPE, stderr=subprocess.PIPE,
482                              close_fds=True)
483         p.stdin.close()
484         line = p.stdout.readline()
485         if not line:
486             sys.stderr.write(p.stderr.read())
487         p.stdout.close()
488         p.stderr.close()
489         ret = p.wait()
490
491         (space, inodes) = line.split()
492         self.disk_inodes = int(inodes)
493         self.disk_blocks = int(space)
494         #(self.disk_inodes, self.disk_blocks) = vduimpl.vdu(self.dir)
495
496         return self.disk_blocks * 1024
497
498     def stop(self, signal = signal.SIGKILL):
499         vserverimpl.killall(self.ctx, signal)
500         self.vm_running = False
501         self.rlimits_changed = False
502
503
504
505 def create(vm_name, static = False, ctor = VServer):
506
507     options = ['vuseradd']
508     if static:
509         options += ['--static']
510     ret = os.spawnvp(os.P_WAIT, 'vuseradd', options + [vm_name])
511     if not os.WIFEXITED(ret) or os.WEXITSTATUS(ret) != 0:
512         out = "system command ('%s') " % options
513         if os.WIFEXITED(ret):
514             out += "failed, rc = %d" % os.WEXITSTATUS(ret)
515         else:
516             out += "killed by signal %d" % os.WTERMSIG(ret)
517         raise SystemError, out
518     vm_id = pwd.getpwnam(vm_name)[2]
519
520     return ctor(vm_name, vm_id)