Don't wait for the initscripts to complete.
[util-vserver.git] / python / vserver.py
index 6bddee1..de8c164 100644 (file)
@@ -1,5 +1,7 @@
 # Copyright 2005 Princeton University
 
 # Copyright 2005 Princeton University
 
+#$Id: vserver.py,v 1.72 2007/08/02 16:01:59 dhozac Exp $
+
 import errno
 import fcntl
 import os
 import errno
 import fcntl
 import os
@@ -9,12 +11,10 @@ import signal
 import sys
 import time
 import traceback
 import sys
 import time
 import traceback
+import subprocess
 import resource
 
 import resource
 
-import mountimpl
-import runcmd
-import utmp
-import vserverimpl, vduimpl
+import vserverimpl
 import cpulimit, bwlimit
 
 from vserverimpl import VS_SCHED_CPU_GUARANTEED as SCHED_CPU_GUARANTEED
 import cpulimit, bwlimit
 
 from vserverimpl import VS_SCHED_CPU_GUARANTEED as SCHED_CPU_GUARANTEED
@@ -54,31 +54,102 @@ for entry in resource.__dict__.keys():
 class NoSuchVServer(Exception): pass
 
 
 class NoSuchVServer(Exception): pass
 
 
+class VServerConfig:
+    def __init__(self, name, directory):
+        self.name = name
+        self.dir = directory
+        self.cache = None
+        if not (os.path.isdir(self.dir) and
+                os.access(self.dir, os.R_OK | os.W_OK | os.X_OK)):
+            raise NoSuchVServer, "%s does not exist" % self.dir
+
+    def get(self, option, default = None):
+        try:
+            if self.cache:
+                return self.cache[option]
+            else:
+                f = open(os.path.join(self.dir, option), "r")
+                buf = f.read().rstrip()
+                f.close()
+                return buf
+        except:
+            if default is not None:
+                return default
+            else:
+                raise KeyError, "Key %s is not set for %s" % (option, self.name)
+
+    def update(self, option, value):
+        if self.cache:
+            return
+
+        try:
+            old_umask = os.umask(0022)
+            filename = os.path.join(self.dir, option)
+            try:
+                os.makedirs(os.path.dirname(filename), 0755)
+            except:
+                pass
+            f = open(filename, 'w')
+            if isinstance(value, list):
+                f.write("%s\n" % "\n".join(value))
+            else:
+                f.write("%s\n" % value)
+            f.close()
+            os.umask(old_umask)
+        except:
+            raise
+
+    def unset(self, option):
+        if self.cache:
+            return
+
+        try:
+            filename = os.path.join(self.dir, option)
+            os.unlink(filename)
+            try:
+                os.removedirs(os.path.dirname(filename))
+            except:
+                pass
+            return True
+        except:
+            return False
+
+    def cache_it(self):
+        self.cache = {}
+        def add_to_cache(cache, dirname, fnames):
+            for file in fnames:
+                full_name = os.path.join(dirname, file)
+                if os.path.islink(full_name):
+                    fnames.remove(file)
+                elif (os.path.isfile(full_name) and
+                      os.access(full_name, os.R_OK)):
+                    f = open(full_name, "r")
+                    cache[full_name.replace(os.path.join(self.dir, ''),
+                                            '')] = f.read().rstrip()
+                    f.close()
+        os.path.walk(self.dir, add_to_cache, self.cache)
+
+
 class VServer:
 
     INITSCRIPTS = [('/etc/rc.vinit', 'start'),
                    ('/etc/rc.d/rc', '%(runlevel)d')]
 
 class VServer:
 
     INITSCRIPTS = [('/etc/rc.vinit', 'start'),
                    ('/etc/rc.d/rc', '%(runlevel)d')]
 
-    def __init__(self, name, vm_id = None, vm_running = False):
+    def __init__(self, name, vm_id = None, vm_running = None):
 
         self.name = name
         self.rlimits_changed = False
 
         self.name = name
         self.rlimits_changed = False
-        self.config_file = "/etc/vservers/%s.conf" % name
         self.dir = "%s/%s" % (vserverimpl.VSERVER_BASEDIR, name)
         if not (os.path.isdir(self.dir) and
                 os.access(self.dir, os.R_OK | os.W_OK | os.X_OK)):
             raise NoSuchVServer, "no such vserver: " + name
         self.dir = "%s/%s" % (vserverimpl.VSERVER_BASEDIR, name)
         if not (os.path.isdir(self.dir) and
                 os.access(self.dir, os.R_OK | os.W_OK | os.X_OK)):
             raise NoSuchVServer, "no such vserver: " + name
-        self.config = {}
-        for config_file in ["/etc/vservers.conf", self.config_file]:
-            try:
-                self.config.update(self.__read_config_file(config_file))
-            except IOError, ex:
-                if ex.errno != errno.ENOENT:
-                    raise
+        self.config = VServerConfig(name, "/etc/vservers/%s" % name)
         self.remove_caps = ~vserverimpl.CAP_SAFE;
         if vm_id == None:
         self.remove_caps = ~vserverimpl.CAP_SAFE;
         if vm_id == None:
-            vm_id = int(self.config['S_CONTEXT'])
+            vm_id = int(self.config.get('context'))
         self.ctx = vm_id
         self.ctx = vm_id
+        if vm_running == None:
+            vm_running = self.is_running()
         self.vm_running = vm_running
 
     def have_limits_changed(self):
         self.vm_running = vm_running
 
     def have_limits_changed(self):
@@ -105,15 +176,12 @@ class VServer:
 
     def set_rlimit_config(self,type,hard,soft,minimum):
         """Generic set resource limit function for vserver"""
 
     def set_rlimit_config(self,type,hard,soft,minimum):
         """Generic set resource limit function for vserver"""
-        resources = {}
         if hard <> VC_LIM_KEEP:
         if hard <> VC_LIM_KEEP:
-            resources["VS_%s_HARD"%type] = hard
+            self.config.update('rlimits/%s.hard' % type.lower(), hard)
         if soft <> VC_LIM_KEEP:
         if soft <> VC_LIM_KEEP:
-            resources["VS_%s_SOFT"%type] = soft
+            self.config.update('rlimits/%s.soft' % type.lower(), soft)
         if minimum <> VC_LIM_KEEP:
         if minimum <> VC_LIM_KEEP:
-            resources["VS_%s_MINIMUM"%type] = minimum
-        if len(resources)>0:
-            self.update_resources(resources)
+            self.config.update('rlimits/%s.min' % type.lower(), minimum)
         self.set_rlimit_limit(type,hard,soft,minimum)
 
     def get_rlimit_limit(self,type):
         self.set_rlimit_limit(type,hard,soft,minimum)
 
     def get_rlimit_limit(self,type):
@@ -129,62 +197,55 @@ class VServer:
 
     def get_rlimit_config(self,type):
         """Generic get resource configuration function for vserver"""
 
     def get_rlimit_config(self,type):
         """Generic get resource configuration function for vserver"""
-        hard = int(self.config.get("VS_%s_HARD"%type,VC_LIM_KEEP))
-        soft = int(self.config.get("VS_%s_SOFT"%type,VC_LIM_KEEP))
-        minimum = int(self.config.get("VS_%s_MINIMUM"%type,VC_LIM_KEEP))
+        hard = int(self.config.get("rlimits/%s.hard"%type.lower(),VC_LIM_KEEP))
+        soft = int(self.config.get("rlimits/%s.soft"%type.lower(),VC_LIM_KEEP))
+        minimum = int(self.config.get("rlimits/%s.min"%type.lower(),VC_LIM_KEEP))
         return (hard,soft,minimum)
 
         return (hard,soft,minimum)
 
-    def set_WHITELISTED_config(self,whitelisted):
-        resources = {'VS_WHITELISTED': whitelisted}
-        self.update_resources(resources)
-
-    config_var_re = re.compile(r"^ *([A-Z_]+)=(.*)\n?$", re.MULTILINE)
-
-    def __read_config_file(self, filename):
-
-        f = open(filename, "r")
-        data = f.read()
-        f.close()
-        config = {}
-        for m in self.config_var_re.finditer(data):
-            (key, val) = m.groups()
-            config[key] = val.strip('"')
-        return config
-
-    def __update_config_file(self, filename, newvars):
-
-        # read old file, apply changes
-        f = open(filename, "r")
-        data = f.read()
-        f.close()
-        todo = newvars.copy()
-        changed = False
-        offset = 0
-        for m in self.config_var_re.finditer(data):
-            (key, val) = m.groups()
-            newval = todo.pop(key, None)
-            if newval != None:
-                data = data[:offset+m.start(2)] + str(newval) + data[offset+m.end(2):]
-                offset += len(str(newval)) - (m.end(2)-m.start(2))
-                changed = True
-        for (newkey, newval) in todo.items():
-            data += "%s=%s\n" % (newkey, newval)
-            changed = True
-
-        if not changed:
-            return
-
-        # write new file
-        newfile = filename + ".new"
-        f = open(newfile, "w")
-        f.write(data)
-        f.close()
-
-        # replace old file with new
-        os.rename(newfile, filename)
+    def set_capabilities(self, capabilities):
+        return vserverimpl.setbcaps(self.ctx, vserverimpl.text2bcaps(capabilities))
+
+    def set_capabilities_config(self, capabilities):
+        self.config.update('bcapabilities', capabilities)
+        self.set_capabilities(capabilities)
+
+    def get_capabilities(self):
+        return vserverimpl.bcaps2text(vserverimpl.getbcaps(self.ctx))
+    def get_capabilities_config(self):
+        return self.config.get('bcapabilities', '')
+
+    def set_ipaddresses(self, addresses):
+        vserverimpl.netremove(self.ctx, "all")
+        for a in addresses.split(","):
+            vserverimpl.netadd(self.ctx, a)
+
+    def set_ipaddresses_config(self, addresses):
+        i = 0
+        for a in addresses.split(","):
+            self.config.update("interfaces/%d/ip" % i, a)
+            i += 1
+        while self.config.unset("interfaces/%d/ip" % i):
+            i += 1
+        self.set_ipaddresses(addresses)
+
+    def get_ipaddresses_config(self):
+        i = 0
+        ret = []
+        while True:
+            r = self.config.get("interfaces/%d/ip" % i, '')
+            if r == '':
+                break
+            ret += [r]
+            i += 1
+        return ",".join(ret)
+
+    def get_ipaddresses(self):
+        # No clean way to do this right now.
+        return None
 
     def __do_chroot(self):
 
     def __do_chroot(self):
-
+        self.config.cache_it()
         os.chroot(self.dir)
         os.chdir("/")
 
         os.chroot(self.dir)
         os.chdir("/")
 
@@ -235,8 +296,7 @@ class VServer:
             print "Unexpected error with setdlimit for context %d" % self.ctx
 
 
             print "Unexpected error with setdlimit for context %d" % self.ctx
 
 
-        resources = {'VS_DISK_MAX': block_limit}
-        self.update_resources(resources)
+        self.config.update('dlimits/0/space_total', block_limit)
 
     def is_running(self):
         return vserverimpl.isrunning(self.ctx)
 
     def is_running(self):
         return vserverimpl.isrunning(self.ctx)
@@ -260,11 +320,13 @@ class VServer:
         configuration file. This method does not modify the kernel CPU
         scheduling parameters for this context. """
 
         configuration file. This method does not modify the kernel CPU
         scheduling parameters for this context. """
 
-        if cpu_share == int(self.config.get("CPULIMIT", -1)):
-            return
-        cpu_guaranteed = sched_flags & SCHED_CPU_GUARANTEED
-        cpu_config = { "CPULIMIT": cpu_share, "CPUGUARANTEED": cpu_guaranteed }
-        self.update_resources(cpu_config)
+        if sched_flags & SCHED_CPU_GUARANTEED:
+            cpu_guaranteed = cpu_share
+        else:
+            cpu_guaranteed = 0
+        self.config.update('sched/fill-rate2', cpu_share)
+        self.config.update('sched/fill-rate', cpu_guaranteed)
+
         if self.vm_running:
             self.set_sched(cpu_share, sched_flags)
 
         if self.vm_running:
             self.set_sched(cpu_share, sched_flags)
 
@@ -301,11 +363,10 @@ class VServer:
     def __do_chcontext(self, state_file):
 
         if state_file:
     def __do_chcontext(self, state_file):
 
         if state_file:
-            print >>state_file, "S_CONTEXT=%u" % self.ctx
-            print >>state_file, "S_PROFILE="
+            print >>state_file, "%u" % self.ctx
             state_file.close()
 
             state_file.close()
 
-        if vserverimpl.chcontext(self.ctx):
+        if vserverimpl.chcontext(self.ctx, vserverimpl.text2bcaps(self.get_capabilities_config())):
             self.set_resources()
             vserverimpl.setup_done(self.ctx)
 
             self.set_resources()
             vserverimpl.setup_done(self.ctx)
 
@@ -332,18 +393,18 @@ class VServer:
 
         # set the initial runlevel
         f = open(RUNDIR + "/utmp", "w")
 
         # set the initial runlevel
         f = open(RUNDIR + "/utmp", "w")
-        utmp.set_runlevel(f, runlevel)
+        vserverimpl.setrunlevel(f, runlevel)
         f.close()
 
         # mount /proc and /dev/pts
         f.close()
 
         # mount /proc and /dev/pts
-        self.__do_mount("none", "/proc", "proc")
+        self.__do_mount("none", self.dir, "/proc", "proc")
         # XXX - magic mount options
         # XXX - magic mount options
-        self.__do_mount("none", "/dev/pts", "devpts", 0, "gid=5,mode=0620")
+        self.__do_mount("none", self.dir, "/dev/pts", "devpts", 0, "gid=5,mode=0620")
 
     def __do_mount(self, *mount_args):
 
         try:
 
     def __do_mount(self, *mount_args):
 
         try:
-            mountimpl.mount(*mount_args)
+            vserverimpl.mount(*mount_args)
         except OSError, ex:
             if ex.errno == errno.EBUSY:
                 # assume already mounted
         except OSError, ex:
             if ex.errno == errno.EBUSY:
                 # assume already mounted
@@ -351,10 +412,8 @@ class VServer:
             raise ex
 
     def enter(self):
             raise ex
 
     def enter(self):
-
-        state_file = open("/var/run/vservers/%s.ctx" % self.name, "w")
         self.__do_chroot()
         self.__do_chroot()
-        self.__do_chcontext(state_file)
+        self.__do_chcontext(None)
 
     def start(self, wait, runlevel = 3):
         self.vm_running = True
 
     def start(self, wait, runlevel = 3):
         self.vm_running = True
@@ -368,14 +427,17 @@ class VServer:
                 os.setsid()
 
                 # open state file to record vserver info
                 os.setsid()
 
                 # open state file to record vserver info
-                state_file = open("/var/run/vservers/%s.ctx" % self.name, "w")
+                state_file = open("/var/run/vservers/%s" % self.name, "w")
 
                 # use /dev/null for stdin, /var/log/boot.log for stdout/err
 
                 # use /dev/null for stdin, /var/log/boot.log for stdout/err
-                os.close(0)
-                os.close(1)
-                os.open("/dev/null", os.O_RDONLY)
+                fd = os.open("/dev/null", os.O_RDONLY)
+                if fd != 0:
+                    os.dup2(fd, 0)
+                    os.close(fd)
                 self.__do_chroot()
                 log = open("/var/log/boot.log", "w", 0)
                 self.__do_chroot()
                 log = open("/var/log/boot.log", "w", 0)
+                if log.fileno() != 1:
+                    os.dup2(log.fileno(), 1)
                 os.dup2(1, 2)
 
                 print >>log, ("%s: starting the virtual server %s" %
                 os.dup2(1, 2)
 
                 print >>log, ("%s: starting the virtual server %s" %
@@ -387,17 +449,17 @@ class VServer:
                 # execute each init script in turn
                 # XXX - we don't support all scripts that vserver script does
                 self.__do_chcontext(state_file)
                 # execute each init script in turn
                 # XXX - we don't support all scripts that vserver script does
                 self.__do_chcontext(state_file)
-               for cmd in self.INITSCRIPTS + [None]:
-                       try:
-                           # enter vserver context
-                           arg_subst = { 'runlevel': runlevel }
-                           cmd_args = [cmd[0]] + map(lambda x: x % arg_subst,
-                                           cmd[1:])
-                           print >>log, "executing '%s'" % " ".join(cmd_args)
-                           os.spawnvp(os.P_WAIT,cmd[0],*cmd_args)
-                       except:
-                               traceback.print_exc()
-                               os._exit(1)
+                for cmd in self.INITSCRIPTS:
+                     try:
+                         # enter vserver context
+                         arg_subst = { 'runlevel': runlevel }
+                         cmd_args = [cmd[0]] + map(lambda x: x % arg_subst,
+                                                   cmd[1:])
+                         print >>log, "executing '%s'" % " ".join(cmd_args)
+                         os.spawnvp(os.P_NOWAIT,cmd[0],cmd_args)
+                     except:
+                         traceback.print_exc()
+                         os._exit(1)
 
             # we get here due to an exception in the top-level child process
             except Exception, ex:
 
             # we get here due to an exception in the top-level child process
             except Exception, ex:
@@ -414,18 +476,25 @@ class VServer:
 
         pass
 
 
         pass
 
-    def update_resources(self, resources):
-
-        self.config.update(resources)
-
-        # write new values to configuration file
-        self.__update_config_file(self.config_file, resources)
-
     def init_disk_info(self):
     def init_disk_info(self):
-
-        (self.disk_inodes, self.disk_blocks, size) = vduimpl.vdu(self.dir)
-
-        return size
+        cmd = "/usr/sbin/vdu --script --space --inodes --blocksize 1024 --xid %d %s" % (self.ctx, self.dir)
+        p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
+                             stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+                             close_fds=True)
+        p.stdin.close()
+        line = p.stdout.readline()
+        if not line:
+            sys.stderr.write(p.stderr.read())
+        p.stdout.close()
+        p.stderr.close()
+        ret = p.wait()
+
+        (space, inodes) = line.split()
+        self.disk_inodes = int(inodes)
+        self.disk_blocks = int(space)
+        #(self.disk_inodes, self.disk_blocks) = vduimpl.vdu(self.dir)
+
+        return self.disk_blocks * 1024
 
     def stop(self, signal = signal.SIGKILL):
         vserverimpl.killall(self.ctx, signal)
 
     def stop(self, signal = signal.SIGKILL):
         vserverimpl.killall(self.ctx, signal)
@@ -436,10 +505,17 @@ class VServer:
 
 def create(vm_name, static = False, ctor = VServer):
 
 
 def create(vm_name, static = False, ctor = VServer):
 
-    options = []
+    options = ['vuseradd']
     if static:
         options += ['--static']
     if static:
         options += ['--static']
-    runcmd.run('vuseradd', options + [vm_name])
+    ret = os.spawnvp(os.P_WAIT, 'vuseradd', options + [vm_name])
+    if !os.WIFEXITED(ret) || os.WEXITSTATUS(ret) != 0:
+        out = "system command ('%s') " % options
+        if os.WIFEXITED(ret):
+            out += "failed, rc = %d" % os.WEXITSTATUS(ret)
+        else:
+            out += "killed by signal %d" % os.WTERMSIG(ret)
+        raise SystemError, out
     vm_id = pwd.getpwnam(vm_name)[2]
 
     return ctor(vm_name, vm_id)
     vm_id = pwd.getpwnam(vm_name)[2]
 
     return ctor(vm_name, vm_id)