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