Added CVS Id field.
[util-vserver.git] / python / vserver.py
index c83c3de..f6eca32 100644 (file)
@@ -1,5 +1,7 @@
 # Copyright 2005 Princeton University
 
 # Copyright 2005 Princeton University
 
+$Id$
+
 import errno
 import fcntl
 import os
 import errno
 import fcntl
 import os
@@ -13,7 +15,7 @@ import traceback
 import mountimpl
 import runcmd
 import utmp
 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
@@ -46,161 +48,149 @@ FLAGS_HIDEINFO = 32
 FLAGS_ULIMIT = 64
 FLAGS_NAMESPACE = 128
 
 FLAGS_ULIMIT = 64
 FLAGS_NAMESPACE = 128
 
+RLIMITS = {"CPU": RLIMIT_CPU,
+           "RSS": RLIMIT_RSS,
+           "NPROC": RLIMIT_NPROC,
+           "NOFILE": RLIMIT_NOFILE,
+           "MEMLOCK": RLIMIT_MEMLOCK,
+           "AS": RLIMIT_AS,
+           "LOCKS": RLIMIT_LOCKS,
+           "SIGPENDING": RLIMIT_SIGPENDING,
+           "MSGQUEUE": RLIMIT_MSGQUEUE,
+           "NSOCK": VLIMIT_NSOCK,
+           "OPENFD": VLIMIT_OPENFD,
+           "ANON": VLIMIT_ANON,
+           "SHMEM": VLIMIT_SHMEM}
 
 class NoSuchVServer(Exception): pass
 
 
 
 class NoSuchVServer(Exception): pass
 
 
+class VServerConfig:
+    def __init__(self, name, directory):
+        self.name = name
+        self.dir = directory
+
+    def get(self, option, default = None):
+        try:
+            f = open(os.path.join(self.dir, option), "r")
+            buf = f.readline().rstrip()
+            f.close()
+            return buf
+        except KeyError, e:
+            # No mapping exists for this option
+            raise e
+        except IOError, e:
+            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):
+        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 KeyError, e:
+            raise KeyError, "Don't know how to handle %s, sorry" % option
+
+
 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.name = name
-        self.config_file = "/etc/vservers/%s.conf" % name
+        self.rlimits_changed = False
         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
 
         self.vm_running = vm_running
 
-        # For every resource key listed in the limit table, add in a
-        # new method with which one can get/set the resource's hard,
-        # soft, minimum limits.
-        limits = {"CPU": RLIMIT_CPU,
-                  "RSS": RLIMIT_RSS,
-                  "NPROC": RLIMIT_NPROC,
-                  "NOFILE": RLIMIT_NOFILE,
-                  "MEMLOCK": RLIMIT_MEMLOCK,
-                  "AS": RLIMIT_AS,
-                  "LOCKS": RLIMIT_LOCKS,
-                  "SIGPENDING": RLIMIT_SIGPENDING,
-                  "MSGQUEUE": RLIMIT_MSGQUEUE,
-                  "NSOCK": VLIMIT_NSOCK,
-                  "OPENFD": VLIMIT_OPENFD,
-                  "ANON": VLIMIT_ANON,
-                  "SHMEM": VLIMIT_SHMEM}
-        for meth in limits.keys():
-            resource_type = limits[meth]
-            func = lambda \
-                       hard=VC_LIM_KEEP,\
-                       soft=VC_LIM_KEEP,\
-                       minimum=VC_LIM_KEEP:\
-                       self.__set_vserver_limit(resource_type,\
-                                                hard, \
-                                                soft,\
-                                                minimum)
-            self.__dict__["set_%s_limit"%meth] = func
-
-            func = lambda \
-                       hard=VC_LIM_KEEP,\
-                       soft=VC_LIM_KEEP,\
-                       minimum=VC_LIM_KEEP:\
-                       self.__set_vserver_config(meth, resource_type, \
-                                                hard, \
-                                                soft,\
-                                                minimum)
-            self.__dict__["set_%s_config"%meth] = func
-
-            func = lambda : self.__get_vserver_limit(resource_type)
-            self.__dict__["get_%s_limit"%meth] = func
-
-            func = lambda : self.__get_vserver_config(meth,resource_type)
-            self.__dict__["get_%s_config"%meth] = func
-    
-    def __set_vserver_limit(self,resource_type,hard,soft,minimum):
+    def have_limits_changed(self):
+        return self.rlimits_changed
+
+    def set_rlimit_limit(self,type,hard,soft,minimum):
         """Generic set resource limit function for vserver"""
         """Generic set resource limit function for vserver"""
-        if self.is_running():
+        global RLIMITS
+        changed = False
+        try:
+            old_hard, old_soft, old_minimum = self.get_rlimit_limit(type)
+            if old_hard != VC_LIM_KEEP and old_hard <> hard: changed = True
+            if old_soft != VC_LIM_KEEP and old_soft <> soft: changed = True
+            if old_minimum != VC_LIM_KEEP and old_minimum <> minimum: changed = True
+            self.rlimits_changed = self.rlimits_changed or changed 
+        except OSError, e:
+            if self.is_running(): print "Unexpected error with getrlimit for running context %d" % self.ctx
+
+        resource_type = RLIMITS[type]
+        try:
             ret = vserverimpl.setrlimit(self.ctx,resource_type,hard,soft,minimum)
             ret = vserverimpl.setrlimit(self.ctx,resource_type,hard,soft,minimum)
+        except OSError, e:
+            if self.is_running(): print "Unexpected error with setrlimit for running context %d" % self.ctx
 
 
-    def __set_vserver_config(self,meth,resource_type,hard,soft,minimum):
+    def set_rlimit_config(self,type,hard,soft,minimum):
         """Generic set resource limit function for vserver"""
         """Generic set resource limit function for vserver"""
-        resources = {}
         if hard <> VC_LIM_KEEP:
         if hard <> VC_LIM_KEEP:
-            resources["VS_%s_HARD"%meth] = hard
+            self.config.update('rlimits/%s.hard' % type.lower(), hard)
         if soft <> VC_LIM_KEEP:
         if soft <> VC_LIM_KEEP:
-            resources["VS_%s_SOFT"%meth] = soft
+            self.config.update('rlimits/%s.soft' % type.lower(), soft)
         if minimum <> VC_LIM_KEEP:
         if minimum <> VC_LIM_KEEP:
-            resources["VS_%s_MINIMUM"%meth] = minimum
-        if len(resources)>0:
-            self.update_resources(resources)
-        self.__set_vserver_limit(resource_type,hard,soft,minimum)
+            self.config.update('rlimits/%s.min' % type.lower(), minimum)
+        self.set_rlimit_limit(type,hard,soft,minimum)
 
 
-    def __get_vserver_limit(self,resource_type):
+    def get_rlimit_limit(self,type):
         """Generic get resource configuration function for vserver"""
         """Generic get resource configuration function for vserver"""
-        if self.is_running():
+        global RLIMITS
+        resource_type = RLIMITS[type]
+        try:
             ret = vserverimpl.getrlimit(self.ctx,resource_type)
             ret = vserverimpl.getrlimit(self.ctx,resource_type)
-        else:
-            ret = __get_vserver_config(meth,resource_type)
+        except OSError, e:
+            print "Unexpected error with getrlimit for context %d" % self.ctx
+            ret = self.get_rlimit_config(type)
         return ret
 
         return ret
 
-    def __get_vserver_config(self,meth,resource_type):
+    def get_rlimit_config(self,type):
         """Generic get resource configuration function for vserver"""
         """Generic get resource configuration function for vserver"""
-        hard = int(self.config.get("VS_%s_HARD"%meth,VC_LIM_KEEP))
-        soft = int(self.config.get("VS_%s_SOFT"%meth,VC_LIM_KEEP))
-        minimum = int(self.config.get("VS_%s_MINIMUM"%meth,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)
 
     def set_WHITELISTED_config(self,whitelisted):
         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):
+        self.config.update('whitelisted', whitelisted)
 
 
-        # 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
+    def set_capabilities(self, capabilities):
+        return vserverimpl.setbcaps(self.ctx, vserverimpl.text2bcaps(capabilities))
 
 
-        # write new file
-        newfile = filename + ".new"
-        f = open(newfile, "w")
-        f.write(data)
-        f.close()
+    def set_capabilities_config(self, capabilities):
+        self.config.update('bcapabilities', capabilities)
+       self.set_capabilities(capabilities)
 
 
-        # replace old file with new
-        os.rename(newfile, filename)
+    def get_capabilities(self):
+        return vserverimpl.bcaps2text(vserverimpl.getbcaps(self.ctx))
+    def get_capabilities_config(self):
+        return self.config.get('bcapabilities')
 
     def __do_chroot(self):
 
 
     def __do_chroot(self):
 
@@ -225,10 +215,12 @@ class VServer:
         return result
 
     def set_disklimit(self, block_limit):
         return result
 
     def set_disklimit(self, block_limit):
-
         # block_limit is in kB
         if block_limit == 0:
         # block_limit is in kB
         if block_limit == 0:
-            vserverimpl.unsetdlimit(self.dir, self.ctx)
+            try:
+                vserverimpl.unsetdlimit(self.dir, self.ctx)
+            except OSError, e:
+                print "Unexpected error with unsetdlimit for context %d" % self.ctx
             return
 
         if self.vm_running:
             return
 
         if self.vm_running:
@@ -239,16 +231,23 @@ class VServer:
             block_usage = self.disk_blocks
             inode_usage = self.disk_inodes
 
             block_usage = self.disk_blocks
             inode_usage = self.disk_inodes
 
-        vserverimpl.setdlimit(self.dir,
-                              self.ctx,
-                              block_usage,
-                              block_limit,
-                              inode_usage,
-                              vserverimpl.DLIMIT_INF,  # inode limit
-                              2)   # %age reserved for root
+
+        try:
+            vserverimpl.setdlimit(self.dir,
+                                  self.ctx,
+                                  block_usage,
+                                  block_limit,
+                                  inode_usage,
+                                  vserverimpl.DLIMIT_INF,  # inode limit
+                                  2)   # %age reserved for root
+        except OSError, e:
+            print "Unexpected error with setdlimit for context %d" % self.ctx
+
+
+        self.config.update('dlimits/0/space_total', block_limit)
 
     def is_running(self):
 
     def is_running(self):
-        return self.vm_running and vserverimpl.isrunning(self.ctx)
+        return vserverimpl.isrunning(self.ctx)
     
     def get_disklimit(self):
 
     
     def get_disklimit(self):
 
@@ -269,11 +268,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)
 
@@ -310,11 +311,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)
 
@@ -359,14 +359,12 @@ 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):
 
     def start(self, wait, runlevel = 3):
-
         self.vm_running = True
         self.vm_running = True
+        self.rlimits_changed = False
 
         child_pid = os.fork()
         if child_pid == 0:
 
         child_pid = os.fork()
         if child_pid == 0:
@@ -376,7 +374,7 @@ 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
                 os.close(0)
 
                 # use /dev/null for stdin, /var/log/boot.log for stdout/err
                 os.close(0)
@@ -394,39 +392,18 @@ class VServer:
 
                 # execute each init script in turn
                 # XXX - we don't support all scripts that vserver script does
 
                 # execute each init script in turn
                 # XXX - we don't support all scripts that vserver script does
-                cmd_pid = 0
-                first_child = True
-                for cmd in self.INITSCRIPTS + [None]:
-                    # wait for previous command to terminate, unless it
-                    # is the last one and the caller has specified to wait
-                    if cmd_pid and (cmd != None or wait):
-                        try:
-                            os.waitpid(cmd_pid, 0)
-                        except:
-                            print >>log, "error waiting for %s:" % cmd_pid
-                            traceback.print_exc()
-
-                    # end of list
-                    if cmd == None:
-                        os._exit(0)
-
-                    # fork and exec next command
-                    cmd_pid = os.fork()
-                    if cmd_pid == 0:
-                        try:
-                            # enter vserver context
-                            self.__do_chcontext(state_file)
-                            arg_subst = { 'runlevel': runlevel }
-                            cmd_args = [cmd[0]] + map(lambda x: x % arg_subst,
-                                                      cmd[1:])
-                            print >>log, "executing '%s'" % " ".join(cmd_args)
-                            os.execl(cmd[0], *cmd_args)
-                        except:
-                            traceback.print_exc()
-                            os._exit(1)
-                    else:
-                        # don't want to write state_file multiple times
-                        state_file = None
+                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)
 
             # 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:
@@ -443,23 +420,24 @@ 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)
+        (child_stdin, child_stdout, child_stderr) = os.popen3(cmd)
+        child_stdin.close()
+        line = child_stdout.readline()
+        if not line:
+            sys.stderr.write(child_stderr.readline())
+        (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):
 
     def stop(self, signal = signal.SIGKILL):
-
         vserverimpl.killall(self.ctx, signal)
         self.vm_running = False
         vserverimpl.killall(self.ctx, signal)
         self.vm_running = False
+        self.rlimits_changed = False