6bddee179fb64b5bc8ec99491bb8fba78aa0ab00
[util-vserver.git] / python / vserver.py
1 # Copyright 2005 Princeton University
2
3 import errno
4 import fcntl
5 import os
6 import re
7 import pwd
8 import signal
9 import sys
10 import time
11 import traceback
12 import resource
13
14 import mountimpl
15 import runcmd
16 import utmp
17 import vserverimpl, vduimpl
18 import cpulimit, bwlimit
19
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
27
28 #
29 # these are the flags taken from the kernel linux/vserver/legacy.h
30 #
31 FLAGS_LOCK = 1
32 FLAGS_SCHED = 2  # XXX - defined in util-vserver/src/chcontext.c
33 FLAGS_NPROC = 4
34 FLAGS_PRIVATE = 8
35 FLAGS_INIT = 16
36 FLAGS_HIDEINFO = 32
37 FLAGS_ULIMIT = 64
38 FLAGS_NAMESPACE = 128
39
40 RLIMITS = { "NSOCK": VLIMIT_NSOCK,
41             "OPENFD": VLIMIT_OPENFD,
42             "ANON": VLIMIT_ANON,
43             "SHMEM": VLIMIT_SHMEM}
44
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]
51         else:
52             print "WARNING: duplicate RLIMITS key %s" % k
53
54 class NoSuchVServer(Exception): pass
55
56
57 class VServer:
58
59     INITSCRIPTS = [('/etc/rc.vinit', 'start'),
60                    ('/etc/rc.d/rc', '%(runlevel)d')]
61
62     def __init__(self, name, vm_id = None, vm_running = False):
63
64         self.name = name
65         self.rlimits_changed = False
66         self.config_file = "/etc/vservers/%s.conf" % name
67         self.dir = "%s/%s" % (vserverimpl.VSERVER_BASEDIR, name)
68         if not (os.path.isdir(self.dir) and
69                 os.access(self.dir, os.R_OK | os.W_OK | os.X_OK)):
70             raise NoSuchVServer, "no such vserver: " + name
71         self.config = {}
72         for config_file in ["/etc/vservers.conf", self.config_file]:
73             try:
74                 self.config.update(self.__read_config_file(config_file))
75             except IOError, ex:
76                 if ex.errno != errno.ENOENT:
77                     raise
78         self.remove_caps = ~vserverimpl.CAP_SAFE;
79         if vm_id == None:
80             vm_id = int(self.config['S_CONTEXT'])
81         self.ctx = vm_id
82         self.vm_running = vm_running
83
84     def have_limits_changed(self):
85         return self.rlimits_changed
86
87     def set_rlimit_limit(self,type,hard,soft,minimum):
88         """Generic set resource limit function for vserver"""
89         global RLIMITS
90         changed = False
91         try:
92             old_hard, old_soft, old_minimum = self.get_rlimit_limit(type)
93             if old_hard != VC_LIM_KEEP and old_hard <> hard: changed = True
94             if old_soft != VC_LIM_KEEP and old_soft <> soft: changed = True
95             if old_minimum != VC_LIM_KEEP and old_minimum <> minimum: changed = True
96             self.rlimits_changed = self.rlimits_changed or changed 
97         except OSError, e:
98             if self.is_running(): print "Unexpected error with getrlimit for running context %d" % self.ctx
99
100         resource_type = RLIMITS[type]
101         try:
102             ret = vserverimpl.setrlimit(self.ctx,resource_type,hard,soft,minimum)
103         except OSError, e:
104             if self.is_running(): print "Unexpected error with setrlimit for running context %d" % self.ctx
105
106     def set_rlimit_config(self,type,hard,soft,minimum):
107         """Generic set resource limit function for vserver"""
108         resources = {}
109         if hard <> VC_LIM_KEEP:
110             resources["VS_%s_HARD"%type] = hard
111         if soft <> VC_LIM_KEEP:
112             resources["VS_%s_SOFT"%type] = soft
113         if minimum <> VC_LIM_KEEP:
114             resources["VS_%s_MINIMUM"%type] = minimum
115         if len(resources)>0:
116             self.update_resources(resources)
117         self.set_rlimit_limit(type,hard,soft,minimum)
118
119     def get_rlimit_limit(self,type):
120         """Generic get resource configuration function for vserver"""
121         global RLIMITS
122         resource_type = RLIMITS[type]
123         try:
124             ret = vserverimpl.getrlimit(self.ctx,resource_type)
125         except OSError, e:
126             print "Unexpected error with getrlimit for context %d" % self.ctx
127             ret = self.get_rlimit_config(type)
128         return ret
129
130     def get_rlimit_config(self,type):
131         """Generic get resource configuration function for vserver"""
132         hard = int(self.config.get("VS_%s_HARD"%type,VC_LIM_KEEP))
133         soft = int(self.config.get("VS_%s_SOFT"%type,VC_LIM_KEEP))
134         minimum = int(self.config.get("VS_%s_MINIMUM"%type,VC_LIM_KEEP))
135         return (hard,soft,minimum)
136
137     def set_WHITELISTED_config(self,whitelisted):
138         resources = {'VS_WHITELISTED': whitelisted}
139         self.update_resources(resources)
140
141     config_var_re = re.compile(r"^ *([A-Z_]+)=(.*)\n?$", re.MULTILINE)
142
143     def __read_config_file(self, filename):
144
145         f = open(filename, "r")
146         data = f.read()
147         f.close()
148         config = {}
149         for m in self.config_var_re.finditer(data):
150             (key, val) = m.groups()
151             config[key] = val.strip('"')
152         return config
153
154     def __update_config_file(self, filename, newvars):
155
156         # read old file, apply changes
157         f = open(filename, "r")
158         data = f.read()
159         f.close()
160         todo = newvars.copy()
161         changed = False
162         offset = 0
163         for m in self.config_var_re.finditer(data):
164             (key, val) = m.groups()
165             newval = todo.pop(key, None)
166             if newval != None:
167                 data = data[:offset+m.start(2)] + str(newval) + data[offset+m.end(2):]
168                 offset += len(str(newval)) - (m.end(2)-m.start(2))
169                 changed = True
170         for (newkey, newval) in todo.items():
171             data += "%s=%s\n" % (newkey, newval)
172             changed = True
173
174         if not changed:
175             return
176
177         # write new file
178         newfile = filename + ".new"
179         f = open(newfile, "w")
180         f.write(data)
181         f.close()
182
183         # replace old file with new
184         os.rename(newfile, filename)
185
186     def __do_chroot(self):
187
188         os.chroot(self.dir)
189         os.chdir("/")
190
191     def chroot_call(self, fn, *args):
192
193         cwd_fd = os.open(".", os.O_RDONLY)
194         try:
195             root_fd = os.open("/", os.O_RDONLY)
196             try:
197                 self.__do_chroot()
198                 result = fn(*args)
199             finally:
200                 os.fchdir(root_fd)
201                 os.chroot(".")
202                 os.fchdir(cwd_fd)
203                 os.close(root_fd)
204         finally:
205             os.close(cwd_fd)
206         return result
207
208     def set_disklimit(self, block_limit):
209         # block_limit is in kB
210         if block_limit == 0:
211             try:
212                 vserverimpl.unsetdlimit(self.dir, self.ctx)
213             except OSError, e:
214                 print "Unexpected error with unsetdlimit for context %d" % self.ctx
215             return
216
217         if self.vm_running:
218             block_usage = vserverimpl.DLIMIT_KEEP
219             inode_usage = vserverimpl.DLIMIT_KEEP
220         else:
221             # init_disk_info() must have been called to get usage values
222             block_usage = self.disk_blocks
223             inode_usage = self.disk_inodes
224
225
226         try:
227             vserverimpl.setdlimit(self.dir,
228                                   self.ctx,
229                                   block_usage,
230                                   block_limit,
231                                   inode_usage,
232                                   vserverimpl.DLIMIT_INF,  # inode limit
233                                   2)   # %age reserved for root
234         except OSError, e:
235             print "Unexpected error with setdlimit for context %d" % self.ctx
236
237
238         resources = {'VS_DISK_MAX': block_limit}
239         self.update_resources(resources)
240
241     def is_running(self):
242         return vserverimpl.isrunning(self.ctx)
243     
244     def get_disklimit(self):
245
246         try:
247             (self.disk_blocks, block_limit, self.disk_inodes, inode_limit,
248              reserved) = vserverimpl.getdlimit(self.dir, self.ctx)
249         except OSError, ex:
250             if ex.errno != errno.ESRCH:
251                 raise
252             # get here if no vserver disk limit has been set for xid
253             block_limit = -1
254
255         return block_limit
256
257     def set_sched_config(self, cpu_share, sched_flags):
258
259         """ Write current CPU scheduler parameters to the vserver
260         configuration file. This method does not modify the kernel CPU
261         scheduling parameters for this context. """
262
263         if cpu_share == int(self.config.get("CPULIMIT", -1)):
264             return
265         cpu_guaranteed = sched_flags & SCHED_CPU_GUARANTEED
266         cpu_config = { "CPULIMIT": cpu_share, "CPUGUARANTEED": cpu_guaranteed }
267         self.update_resources(cpu_config)
268         if self.vm_running:
269             self.set_sched(cpu_share, sched_flags)
270
271     def set_sched(self, cpu_share, sched_flags = 0):
272         """ Update kernel CPU scheduling parameters for this context. """
273         vserverimpl.setsched(self.ctx, cpu_share, sched_flags)
274
275     def get_sched(self):
276         # have no way of querying scheduler right now on a per vserver basis
277         return (-1, False)
278
279     def set_bwlimit(self, minrate = bwlimit.bwmin, maxrate = None,
280                     exempt_min = None, exempt_max = None,
281                     share = None, dev = "eth0"):
282
283         if minrate is None:
284             bwlimit.off(self.ctx, dev)
285         else:
286             bwlimit.on(self.ctx, dev, share,
287                        minrate, maxrate, exempt_min, exempt_max)
288
289     def get_bwlimit(self, dev = "eth0"):
290
291         result = bwlimit.get(self.ctx)
292         # result of bwlimit.get is (ctx, share, minrate, maxrate)
293         if result:
294             result = result[1:]
295         return result
296
297     def open(self, filename, mode = "r", bufsize = -1):
298
299         return self.chroot_call(open, filename, mode, bufsize)
300
301     def __do_chcontext(self, state_file):
302
303         if state_file:
304             print >>state_file, "S_CONTEXT=%u" % self.ctx
305             print >>state_file, "S_PROFILE="
306             state_file.close()
307
308         if vserverimpl.chcontext(self.ctx):
309             self.set_resources()
310             vserverimpl.setup_done(self.ctx)
311
312     def __prep(self, runlevel, log):
313
314         """ Perform all the crap that the vserver script does before
315         actually executing the startup scripts. """
316
317         # remove /var/run and /var/lock/subsys files
318         # but don't remove utmp from the top-level /var/run
319         RUNDIR = "/var/run"
320         LOCKDIR = "/var/lock/subsys"
321         filter_fn = lambda fs: filter(lambda f: f != 'utmp', fs)
322         garbage = reduce((lambda (out, ff), (dir, subdirs, files):
323                           (out + map((dir + "/").__add__, ff(files)),
324                            lambda fs: fs)),
325                          list(os.walk(RUNDIR)),
326                          ([], filter_fn))[0]
327         garbage += filter(os.path.isfile, map((LOCKDIR + "/").__add__,
328                                               os.listdir(LOCKDIR)))
329         if False:
330             for f in garbage:
331                 os.unlink(f)
332
333         # set the initial runlevel
334         f = open(RUNDIR + "/utmp", "w")
335         utmp.set_runlevel(f, runlevel)
336         f.close()
337
338         # mount /proc and /dev/pts
339         self.__do_mount("none", "/proc", "proc")
340         # XXX - magic mount options
341         self.__do_mount("none", "/dev/pts", "devpts", 0, "gid=5,mode=0620")
342
343     def __do_mount(self, *mount_args):
344
345         try:
346             mountimpl.mount(*mount_args)
347         except OSError, ex:
348             if ex.errno == errno.EBUSY:
349                 # assume already mounted
350                 return
351             raise ex
352
353     def enter(self):
354
355         state_file = open("/var/run/vservers/%s.ctx" % self.name, "w")
356         self.__do_chroot()
357         self.__do_chcontext(state_file)
358
359     def start(self, wait, runlevel = 3):
360         self.vm_running = True
361         self.rlimits_changed = False
362
363         child_pid = os.fork()
364         if child_pid == 0:
365             # child process
366             try:
367                 # get a new session
368                 os.setsid()
369
370                 # open state file to record vserver info
371                 state_file = open("/var/run/vservers/%s.ctx" % self.name, "w")
372
373                 # use /dev/null for stdin, /var/log/boot.log for stdout/err
374                 os.close(0)
375                 os.close(1)
376                 os.open("/dev/null", os.O_RDONLY)
377                 self.__do_chroot()
378                 log = open("/var/log/boot.log", "w", 0)
379                 os.dup2(1, 2)
380
381                 print >>log, ("%s: starting the virtual server %s" %
382                               (time.asctime(time.gmtime()), self.name))
383
384                 # perform pre-init cleanup
385                 self.__prep(runlevel, log)
386
387                 # execute each init script in turn
388                 # XXX - we don't support all scripts that vserver script does
389                 self.__do_chcontext(state_file)
390                 for cmd in self.INITSCRIPTS + [None]:
391                         try:
392                             # enter vserver context
393                             arg_subst = { 'runlevel': runlevel }
394                             cmd_args = [cmd[0]] + map(lambda x: x % arg_subst,
395                                             cmd[1:])
396                             print >>log, "executing '%s'" % " ".join(cmd_args)
397                             os.spawnvp(os.P_WAIT,cmd[0],*cmd_args)
398                         except:
399                                 traceback.print_exc()
400                                 os._exit(1)
401
402             # we get here due to an exception in the top-level child process
403             except Exception, ex:
404                 traceback.print_exc()
405             os._exit(0)
406
407         # parent process
408         return child_pid
409
410     def set_resources(self):
411
412         """ Called when vserver context is entered for first time,
413         should be overridden by subclass. """
414
415         pass
416
417     def update_resources(self, resources):
418
419         self.config.update(resources)
420
421         # write new values to configuration file
422         self.__update_config_file(self.config_file, resources)
423
424     def init_disk_info(self):
425
426         (self.disk_inodes, self.disk_blocks, size) = vduimpl.vdu(self.dir)
427
428         return size
429
430     def stop(self, signal = signal.SIGKILL):
431         vserverimpl.killall(self.ctx, signal)
432         self.vm_running = False
433         self.rlimits_changed = False
434
435
436
437 def create(vm_name, static = False, ctor = VServer):
438
439     options = []
440     if static:
441         options += ['--static']
442     runcmd.run('vuseradd', options + [vm_name])
443     vm_id = pwd.getpwnam(vm_name)[2]
444
445     return ctor(vm_name, vm_id)