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, logfile=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
154 self.logfile = logfile
156 # inspired from nodemanager's logger
160 fd = os.open(self.logfile,"a", 0600)
161 if not msg.endswith('\n'): msg += '\n'
162 os.write(fd, '%s: %s' % (time.asctime(time.gmtime()), msg))
167 def have_limits_changed(self):
168 return self.rlimits_changed
170 def set_rlimit_limit(self,type,hard,soft,minimum):
171 """Generic set resource limit function for vserver"""
175 old_hard, old_soft, old_minimum = self.get_rlimit_limit(type)
176 if old_hard != VC_LIM_KEEP and old_hard <> hard: changed = True
177 if old_soft != VC_LIM_KEEP and old_soft <> soft: changed = True
178 if old_minimum != VC_LIM_KEEP and old_minimum <> minimum: changed = True
179 self.rlimits_changed = self.rlimits_changed or changed
181 if self.is_running(): self.log("Unexpected error with getrlimit for running context %d" % self.ctx)
183 resource_type = RLIMITS[type]
185 ret = vserverimpl.setrlimit(self.ctx,resource_type,hard,soft,minimum)
187 if self.is_running(): self.log("Unexpected error with setrlimit for running context %d" % self.ctx)
189 def set_rlimit_config(self,type,hard,soft,minimum):
190 """Generic set resource limit function for vserver"""
191 if hard <> VC_LIM_KEEP:
192 self.config.update('rlimits/%s.hard' % type.lower(), hard)
193 if soft <> VC_LIM_KEEP:
194 self.config.update('rlimits/%s.soft' % type.lower(), soft)
195 if minimum <> VC_LIM_KEEP:
196 self.config.update('rlimits/%s.min' % type.lower(), minimum)
197 self.set_rlimit_limit(type,hard,soft,minimum)
199 def get_rlimit_limit(self,type):
200 """Generic get resource configuration function for vserver"""
202 resource_type = RLIMITS[type]
204 ret = vserverimpl.getrlimit(self.ctx,resource_type)
206 self.log("Unexpected error with getrlimit for context %d" % self.ctx)
207 ret = self.get_rlimit_config(type)
210 def get_rlimit_config(self,type):
211 """Generic get resource configuration function for vserver"""
212 hard = int(self.config.get("rlimits/%s.hard"%type.lower(),VC_LIM_KEEP))
213 soft = int(self.config.get("rlimits/%s.soft"%type.lower(),VC_LIM_KEEP))
214 minimum = int(self.config.get("rlimits/%s.min"%type.lower(),VC_LIM_KEEP))
215 return (hard,soft,minimum)
217 def set_capabilities(self, capabilities):
218 return vserverimpl.setbcaps(self.ctx, vserverimpl.text2bcaps(capabilities))
220 def set_capabilities_config(self, capabilities):
221 self.config.update('bcapabilities', capabilities)
222 self.set_capabilities(capabilities)
224 def get_capabilities(self):
225 return vserverimpl.bcaps2text(vserverimpl.getbcaps(self.ctx))
227 def get_capabilities_config(self):
228 return self.config.get('bcapabilities', '')
230 def set_ipaddresses(self, addresses):
231 vserverimpl.netremove(self.ctx, "all")
232 for a in addresses.split(","):
233 vserverimpl.netadd(self.ctx, a)
235 def set_ipaddresses_config(self, addresses):
237 for a in addresses.split(","):
238 self.config.update("interfaces/%d/ip" % i, a)
240 while self.config.unset("interfaces/%d/ip" % i):
242 self.set_ipaddresses(addresses)
244 def get_ipaddresses_config(self):
248 r = self.config.get("interfaces/%d/ip" % i, '')
255 def get_ipaddresses(self):
256 # No clean way to do this right now.
259 def __do_chroot(self):
263 def chroot_call(self, fn, *args):
265 cwd_fd = os.open(".", os.O_RDONLY)
267 root_fd = os.open("/", os.O_RDONLY)
280 def set_disklimit(self, block_limit):
281 # block_limit is in kB
284 vserverimpl.unsetdlimit(self.dir, self.ctx)
286 self.log("Unexpected error with unsetdlimit for context %d" % self.ctx)
290 block_usage = vserverimpl.DLIMIT_KEEP
291 inode_usage = vserverimpl.DLIMIT_KEEP
293 # init_disk_info() must have been called to get usage values
294 block_usage = self.disk_blocks
295 inode_usage = self.disk_inodes
299 vserverimpl.setdlimit(self.dir,
304 vserverimpl.DLIMIT_INF, # inode limit
305 2) # %age reserved for root
307 self.log("Unexpected error with setdlimit for context %d" % self.ctx)
310 self.config.update('dlimits/0/space_total', block_limit)
312 def is_running(self):
313 return vserverimpl.isrunning(self.ctx)
315 def get_disklimit(self):
318 (self.disk_blocks, block_limit, self.disk_inodes, inode_limit,
319 reserved) = vserverimpl.getdlimit(self.dir, self.ctx)
321 if ex.errno != errno.ESRCH:
323 # get here if no vserver disk limit has been set for xid
328 def set_sched_config(self, cpu_share, sched_flags):
330 """ Write current CPU scheduler parameters to the vserver
331 configuration file. This method does not modify the kernel CPU
332 scheduling parameters for this context. """
334 if sched_flags & SCHED_CPU_GUARANTEED:
335 cpu_guaranteed = cpu_share
338 self.config.update('sched/fill-rate2', cpu_share)
339 self.config.update('sched/fill-rate', cpu_guaranteed)
342 self.set_sched(cpu_share, sched_flags)
344 def set_sched(self, cpu_share, sched_flags = 0):
345 """ Update kernel CPU scheduling parameters for this context. """
346 vserverimpl.setsched(self.ctx, cpu_share, sched_flags)
349 # have no way of querying scheduler right now on a per vserver basis
352 def set_bwlimit(self, minrate = bwlimit.bwmin, maxrate = None,
353 exempt_min = None, exempt_max = None,
354 share = None, dev = "eth0"):
357 bwlimit.off(self.ctx, dev)
359 bwlimit.on(self.ctx, dev, share,
360 minrate, maxrate, exempt_min, exempt_max)
362 def get_bwlimit(self, dev = "eth0"):
364 result = bwlimit.get(self.ctx)
365 # result of bwlimit.get is (ctx, share, minrate, maxrate)
370 def open(self, filename, mode = "r", bufsize = -1):
372 return self.chroot_call(open, filename, mode, bufsize)
374 def __do_chcontext(self, state_file):
377 print >>state_file, "%u" % self.ctx
380 if vserverimpl.chcontext(self.ctx, vserverimpl.text2bcaps(self.get_capabilities_config())):
382 vserverimpl.setup_done(self.ctx)
384 def __prep(self, runlevel, log):
386 """ Perform all the crap that the vserver script does before
387 actually executing the startup scripts. """
389 # remove /var/run and /var/lock/subsys files
390 # but don't remove utmp from the top-level /var/run
392 LOCKDIR = "/var/lock/subsys"
393 filter_fn = lambda fs: filter(lambda f: f != 'utmp', fs)
394 garbage = reduce((lambda (out, ff), (dir, subdirs, files):
395 (out + map((dir + "/").__add__, ff(files)),
397 list(os.walk(RUNDIR)),
399 garbage += filter(os.path.isfile, map((LOCKDIR + "/").__add__,
400 os.listdir(LOCKDIR)))
405 # set the initial runlevel
406 vserverimpl.setrunlevel(RUNDIR + "/utmp", runlevel)
408 # mount /proc and /dev/pts
409 self.__do_mount("none", self.dir, "/proc", "proc")
410 # XXX - magic mount options
411 self.__do_mount("none", self.dir, "/dev/pts", "devpts", 0, "gid=5,mode=0620")
413 def __do_mount(self, *mount_args):
416 vserverimpl.mount(*mount_args)
418 if ex.errno == errno.EBUSY:
419 # assume already mounted
424 self.config.cache_it()
426 self.__do_chcontext(None)
428 def start(self, wait, runlevel = 3):
429 self.vm_running = True
430 self.rlimits_changed = False
432 child_pid = os.fork()
439 # open state file to record vserver info
440 state_file = open("/var/run/vservers/%s" % self.name, "w")
442 # use /dev/null for stdin, /var/log/boot.log for stdout/err
443 fd = os.open("/dev/null", os.O_RDONLY)
447 self.config.cache_it()
449 log = open("/var/log/boot.log", "a", 0)
450 if log.fileno() != 1:
451 os.dup2(log.fileno(), 1)
454 print >>log, ("%s: starting the virtual server %s" %
455 (time.asctime(time.gmtime()), self.name))
457 # perform pre-init cleanup
458 self.__prep(runlevel, log)
460 # execute each init script in turn
461 # XXX - we don't support all scripts that vserver script does
462 self.__do_chcontext(state_file)
463 for cmd in self.INITSCRIPTS:
465 # enter vserver context
466 arg_subst = { 'runlevel': runlevel }
467 cmd_args = [cmd[0]] + map(lambda x: x % arg_subst,
469 print >>log, "executing '%s'" % " ".join(cmd_args)
470 os.spawnvp(os.P_NOWAIT,cmd[0],cmd_args)
472 print >>log, traceback.format_exc()
475 # we get here due to an exception in the top-level child process
476 except Exception, ex:
477 self.log(traceback.format_exc())
483 def set_resources(self):
485 """ Called when vserver context is entered for first time,
486 should be overridden by subclass. """
490 def init_disk_info(self):
491 cmd = "/usr/sbin/vdu --script --space --inodes --blocksize 1024 --xid %d %s" % (self.ctx, self.dir)
492 p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
493 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
496 line = p.stdout.readline()
498 sys.stderr.write(p.stderr.read())
503 (space, inodes) = line.split()
504 self.disk_inodes = int(inodes)
505 self.disk_blocks = int(space)
506 #(self.disk_inodes, self.disk_blocks) = vduimpl.vdu(self.dir)
508 return self.disk_blocks * 1024
510 def stop(self, signal = signal.SIGKILL):
511 vserverimpl.killall(self.ctx, signal)
512 self.vm_running = False
513 self.rlimits_changed = False
517 def create(vm_name, static = False, ctor = VServer):
519 options = ['vuseradd']
521 options += ['--static']
522 ret = os.spawnvp(os.P_WAIT, 'vuseradd', options + [vm_name])
523 if not os.WIFEXITED(ret) or os.WEXITSTATUS(ret) != 0:
524 out = "system command ('%s') " % options
525 if os.WIFEXITED(ret):
526 out += "failed, rc = %d" % os.WEXITSTATUS(ret)
528 out += "killed by signal %d" % os.WTERMSIG(ret)
529 raise SystemError, out
530 vm_id = pwd.getpwnam(vm_name)[2]
532 return ctor(vm_name, vm_id)