1 # Copyright 2005 Princeton University
3 #$Id: vserver.py,v 1.72 2007/08/02 16:01:59 dhozac Exp $
18 import cpulimit, bwlimit
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
29 # these are the flags taken from the kernel linux/vserver/legacy.h
32 FLAGS_SCHED = 2 # XXX - defined in util-vserver/src/chcontext.c
40 RLIMITS = { "NSOCK": VLIMIT_NSOCK,
41 "OPENFD": VLIMIT_OPENFD,
43 "SHMEM": VLIMIT_SHMEM}
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]
52 print "WARNING: duplicate RLIMITS key %s" % k
54 class NoSuchVServer(Exception): pass
58 def __init__(self, name, directory):
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
66 def get(self, option, default = None):
69 return self.cache[option]
71 f = open(os.path.join(self.dir, option), "r")
72 buf = f.read().rstrip()
76 if default is not None:
79 raise KeyError, "Key %s is not set for %s" % (option, self.name)
81 def update(self, option, value):
86 old_umask = os.umask(0022)
87 filename = os.path.join(self.dir, option)
89 os.makedirs(os.path.dirname(filename), 0755)
92 f = open(filename, 'w')
93 if isinstance(value, list):
94 f.write("%s\n" % "\n".join(value))
96 f.write("%s\n" % value)
102 def unset(self, option):
107 filename = os.path.join(self.dir, option)
110 os.removedirs(os.path.dirname(filename))
119 def add_to_cache(cache, dirname, fnames):
121 full_name = os.path.join(dirname, file)
122 if os.path.islink(full_name):
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()
130 os.path.walk(self.dir, add_to_cache, self.cache)
135 INITSCRIPTS = [('/etc/rc.vinit', 'start'),
136 ('/etc/rc.d/rc', '%(runlevel)d')]
138 def __init__(self, name, vm_id = None, vm_running = None):
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;
149 vm_id = int(self.config.get('context'))
151 if vm_running == None:
152 vm_running = self.is_running()
153 self.vm_running = vm_running
155 def have_limits_changed(self):
156 return self.rlimits_changed
158 def set_rlimit_limit(self,type,hard,soft,minimum):
159 """Generic set resource limit function for vserver"""
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
169 if self.is_running(): print "Unexpected error with getrlimit for running context %d" % self.ctx
171 resource_type = RLIMITS[type]
173 ret = vserverimpl.setrlimit(self.ctx,resource_type,hard,soft,minimum)
175 if self.is_running(): print "Unexpected error with setrlimit for running context %d" % self.ctx
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)
187 def get_rlimit_limit(self,type):
188 """Generic get resource configuration function for vserver"""
190 resource_type = RLIMITS[type]
192 ret = vserverimpl.getrlimit(self.ctx,resource_type)
194 print "Unexpected error with getrlimit for context %d" % self.ctx
195 ret = self.get_rlimit_config(type)
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)
205 def set_capabilities(self, capabilities):
206 return vserverimpl.setbcaps(self.ctx, vserverimpl.text2bcaps(capabilities))
208 def set_capabilities_config(self, capabilities):
209 self.config.update('bcapabilities', capabilities)
210 self.set_capabilities(capabilities)
212 def get_capabilities(self):
213 return vserverimpl.bcaps2text(vserverimpl.getbcaps(self.ctx))
215 def get_capabilities_config(self):
216 return self.config.get('bcapabilities', '')
218 def set_ipaddresses(self, addresses):
219 vserverimpl.netremove(self.ctx, "all")
220 for a in addresses.split(","):
221 vserverimpl.netadd(self.ctx, a)
223 def set_ipaddresses_config(self, addresses):
225 for a in addresses.split(","):
226 self.config.update("interfaces/%d/ip" % i, a)
228 while self.config.unset("interfaces/%d/ip" % i):
230 self.set_ipaddresses(addresses)
232 def get_ipaddresses_config(self):
236 r = self.config.get("interfaces/%d/ip" % i, '')
243 def get_ipaddresses(self):
244 # No clean way to do this right now.
247 def __do_chroot(self):
251 def chroot_call(self, fn, *args):
253 cwd_fd = os.open(".", os.O_RDONLY)
255 root_fd = os.open("/", os.O_RDONLY)
268 def set_disklimit(self, block_limit):
269 # block_limit is in kB
272 vserverimpl.unsetdlimit(self.dir, self.ctx)
274 print "Unexpected error with unsetdlimit for context %d" % self.ctx
278 block_usage = vserverimpl.DLIMIT_KEEP
279 inode_usage = vserverimpl.DLIMIT_KEEP
281 # init_disk_info() must have been called to get usage values
282 block_usage = self.disk_blocks
283 inode_usage = self.disk_inodes
287 vserverimpl.setdlimit(self.dir,
292 vserverimpl.DLIMIT_INF, # inode limit
293 2) # %age reserved for root
295 print "Unexpected error with setdlimit for context %d" % self.ctx
298 self.config.update('dlimits/0/space_total', block_limit)
300 def is_running(self):
301 return vserverimpl.isrunning(self.ctx)
303 def get_disklimit(self):
306 (self.disk_blocks, block_limit, self.disk_inodes, inode_limit,
307 reserved) = vserverimpl.getdlimit(self.dir, self.ctx)
309 if ex.errno != errno.ESRCH:
311 # get here if no vserver disk limit has been set for xid
316 def set_sched_config(self, cpu_share, sched_flags):
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. """
322 if sched_flags & SCHED_CPU_GUARANTEED:
323 cpu_guaranteed = cpu_share
326 self.config.update('sched/fill-rate2', cpu_share)
327 self.config.update('sched/fill-rate', cpu_guaranteed)
330 self.set_sched(cpu_share, sched_flags)
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)
337 # have no way of querying scheduler right now on a per vserver basis
340 def set_bwlimit(self, minrate = bwlimit.bwmin, maxrate = None,
341 exempt_min = None, exempt_max = None,
342 share = None, dev = "eth0"):
345 bwlimit.off(self.ctx, dev)
347 bwlimit.on(self.ctx, dev, share,
348 minrate, maxrate, exempt_min, exempt_max)
350 def get_bwlimit(self, dev = "eth0"):
352 result = bwlimit.get(self.ctx)
353 # result of bwlimit.get is (ctx, share, minrate, maxrate)
358 def open(self, filename, mode = "r", bufsize = -1):
360 return self.chroot_call(open, filename, mode, bufsize)
362 def __do_chcontext(self, state_file):
365 print >>state_file, "%u" % self.ctx
368 if vserverimpl.chcontext(self.ctx, vserverimpl.text2bcaps(self.get_capabilities_config())):
370 vserverimpl.setup_done(self.ctx)
372 def __prep(self, runlevel, log):
374 """ Perform all the crap that the vserver script does before
375 actually executing the startup scripts. """
377 # remove /var/run and /var/lock/subsys files
378 # but don't remove utmp from the top-level /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)),
385 list(os.walk(RUNDIR)),
387 garbage += filter(os.path.isfile, map((LOCKDIR + "/").__add__,
388 os.listdir(LOCKDIR)))
393 # set the initial runlevel
394 f = open(RUNDIR + "/utmp", "w")
395 vserverimpl.setrunlevel(f, runlevel)
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")
403 def __do_mount(self, *mount_args):
406 vserverimpl.mount(*mount_args)
408 if ex.errno == errno.EBUSY:
409 # assume already mounted
414 self.config.cache_it()
416 self.__do_chcontext(None)
418 def start(self, wait, runlevel = 3):
419 self.vm_running = True
420 self.rlimits_changed = False
422 child_pid = os.fork()
429 # open state file to record vserver info
430 state_file = open("/var/run/vservers/%s" % self.name, "w")
432 # use /dev/null for stdin, /var/log/boot.log for stdout/err
433 fd = os.open("/dev/null", os.O_RDONLY)
437 self.config.cache_it()
439 log = open("/var/log/boot.log", "w", 0)
440 if log.fileno() != 1:
441 os.dup2(log.fileno(), 1)
444 print >>log, ("%s: starting the virtual server %s" %
445 (time.asctime(time.gmtime()), self.name))
447 # perform pre-init cleanup
448 self.__prep(runlevel, log)
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:
455 # enter vserver context
456 arg_subst = { 'runlevel': runlevel }
457 cmd_args = [cmd[0]] + map(lambda x: x % arg_subst,
459 print >>log, "executing '%s'" % " ".join(cmd_args)
460 os.spawnvp(os.P_NOWAIT,cmd[0],cmd_args)
462 traceback.print_exc()
465 # we get here due to an exception in the top-level child process
466 except Exception, ex:
467 traceback.print_exc()
473 def set_resources(self):
475 """ Called when vserver context is entered for first time,
476 should be overridden by subclass. """
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,
486 line = p.stdout.readline()
488 sys.stderr.write(p.stderr.read())
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)
498 return self.disk_blocks * 1024
500 def stop(self, signal = signal.SIGKILL):
501 vserverimpl.killall(self.ctx, signal)
502 self.vm_running = False
503 self.rlimits_changed = False
507 def create(vm_name, static = False, ctor = VServer):
509 options = ['vuseradd']
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)
518 out += "killed by signal %d" % os.WTERMSIG(ret)
519 raise SystemError, out
520 vm_id = pwd.getpwnam(vm_name)[2]
522 return ctor(vm_name, vm_id)