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