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):
248 self.config.cache_it()
252 def chroot_call(self, fn, *args):
254 cwd_fd = os.open(".", os.O_RDONLY)
256 root_fd = os.open("/", os.O_RDONLY)
269 def set_disklimit(self, block_limit):
270 # block_limit is in kB
273 vserverimpl.unsetdlimit(self.dir, self.ctx)
275 print "Unexpected error with unsetdlimit for context %d" % self.ctx
279 block_usage = vserverimpl.DLIMIT_KEEP
280 inode_usage = vserverimpl.DLIMIT_KEEP
282 # init_disk_info() must have been called to get usage values
283 block_usage = self.disk_blocks
284 inode_usage = self.disk_inodes
288 vserverimpl.setdlimit(self.dir,
293 vserverimpl.DLIMIT_INF, # inode limit
294 2) # %age reserved for root
296 print "Unexpected error with setdlimit for context %d" % self.ctx
299 self.config.update('dlimits/0/space_total', block_limit)
301 def is_running(self):
302 return vserverimpl.isrunning(self.ctx)
304 def get_disklimit(self):
307 (self.disk_blocks, block_limit, self.disk_inodes, inode_limit,
308 reserved) = vserverimpl.getdlimit(self.dir, self.ctx)
310 if ex.errno != errno.ESRCH:
312 # get here if no vserver disk limit has been set for xid
317 def set_sched_config(self, cpu_share, sched_flags):
319 """ Write current CPU scheduler parameters to the vserver
320 configuration file. This method does not modify the kernel CPU
321 scheduling parameters for this context. """
323 if sched_flags & SCHED_CPU_GUARANTEED:
324 cpu_guaranteed = cpu_share
327 self.config.update('sched/fill-rate2', cpu_share)
328 self.config.update('sched/fill-rate', cpu_guaranteed)
331 self.set_sched(cpu_share, sched_flags)
333 def set_sched(self, cpu_share, sched_flags = 0):
334 """ Update kernel CPU scheduling parameters for this context. """
335 vserverimpl.setsched(self.ctx, cpu_share, sched_flags)
338 # have no way of querying scheduler right now on a per vserver basis
341 def set_bwlimit(self, minrate = bwlimit.bwmin, maxrate = None,
342 exempt_min = None, exempt_max = None,
343 share = None, dev = "eth0"):
346 bwlimit.off(self.ctx, dev)
348 bwlimit.on(self.ctx, dev, share,
349 minrate, maxrate, exempt_min, exempt_max)
351 def get_bwlimit(self, dev = "eth0"):
353 result = bwlimit.get(self.ctx)
354 # result of bwlimit.get is (ctx, share, minrate, maxrate)
359 def open(self, filename, mode = "r", bufsize = -1):
361 return self.chroot_call(open, filename, mode, bufsize)
363 def __do_chcontext(self, state_file):
366 print >>state_file, "%u" % self.ctx
369 if vserverimpl.chcontext(self.ctx, vserverimpl.text2bcaps(self.get_capabilities_config())):
371 vserverimpl.setup_done(self.ctx)
373 def __prep(self, runlevel, log):
375 """ Perform all the crap that the vserver script does before
376 actually executing the startup scripts. """
378 # remove /var/run and /var/lock/subsys files
379 # but don't remove utmp from the top-level /var/run
381 LOCKDIR = "/var/lock/subsys"
382 filter_fn = lambda fs: filter(lambda f: f != 'utmp', fs)
383 garbage = reduce((lambda (out, ff), (dir, subdirs, files):
384 (out + map((dir + "/").__add__, ff(files)),
386 list(os.walk(RUNDIR)),
388 garbage += filter(os.path.isfile, map((LOCKDIR + "/").__add__,
389 os.listdir(LOCKDIR)))
394 # set the initial runlevel
395 f = open(RUNDIR + "/utmp", "w")
396 vserverimpl.setrunlevel(f, runlevel)
399 # mount /proc and /dev/pts
400 self.__do_mount("none", self.dir, "/proc", "proc")
401 # XXX - magic mount options
402 self.__do_mount("none", self.dir, "/dev/pts", "devpts", 0, "gid=5,mode=0620")
404 def __do_mount(self, *mount_args):
407 vserverimpl.mount(*mount_args)
409 if ex.errno == errno.EBUSY:
410 # assume already mounted
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)
438 log = open("/var/log/boot.log", "w", 0)
439 if log.fileno() != 1:
440 os.dup2(log.fileno(), 1)
443 print >>log, ("%s: starting the virtual server %s" %
444 (time.asctime(time.gmtime()), self.name))
446 # perform pre-init cleanup
447 self.__prep(runlevel, log)
449 # execute each init script in turn
450 # XXX - we don't support all scripts that vserver script does
451 self.__do_chcontext(state_file)
452 for cmd in self.INITSCRIPTS:
454 # enter vserver context
455 arg_subst = { 'runlevel': runlevel }
456 cmd_args = [cmd[0]] + map(lambda x: x % arg_subst,
458 print >>log, "executing '%s'" % " ".join(cmd_args)
459 os.spawnvp(os.P_NOWAIT,cmd[0],cmd_args)
461 traceback.print_exc()
464 # we get here due to an exception in the top-level child process
465 except Exception, ex:
466 traceback.print_exc()
472 def set_resources(self):
474 """ Called when vserver context is entered for first time,
475 should be overridden by subclass. """
479 def init_disk_info(self):
480 cmd = "/usr/sbin/vdu --script --space --inodes --blocksize 1024 --xid %d %s" % (self.ctx, self.dir)
481 p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
482 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
485 line = p.stdout.readline()
487 sys.stderr.write(p.stderr.read())
492 (space, inodes) = line.split()
493 self.disk_inodes = int(inodes)
494 self.disk_blocks = int(space)
495 #(self.disk_inodes, self.disk_blocks) = vduimpl.vdu(self.dir)
497 return self.disk_blocks * 1024
499 def stop(self, signal = signal.SIGKILL):
500 vserverimpl.killall(self.ctx, signal)
501 self.vm_running = False
502 self.rlimits_changed = False
506 def create(vm_name, static = False, ctor = VServer):
508 options = ['vuseradd']
510 options += ['--static']
511 ret = os.spawnvp(os.P_WAIT, 'vuseradd', options + [vm_name])
512 if not os.WIFEXITED(ret) or os.WEXITSTATUS(ret) != 0:
513 out = "system command ('%s') " % options
514 if os.WIFEXITED(ret):
515 out += "failed, rc = %d" % os.WEXITSTATUS(ret)
517 out += "killed by signal %d" % os.WTERMSIG(ret)
518 raise SystemError, out
519 vm_id = pwd.getpwnam(vm_name)[2]
521 return ctor(vm_name, vm_id)