Fix syntax error when parsing INITSCRIPT tuple.
[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.cache = None
67         self.config_file = "/etc/vservers/%s.conf" % name
68         self.dir = "%s/%s" % (vserverimpl.VSERVER_BASEDIR, name)
69         if not (os.path.isdir(self.dir) and
70                 os.access(self.dir, os.R_OK | os.W_OK | os.X_OK)):
71             raise NoSuchVServer, "no such vserver: " + name
72         self.config = {}
73         for config_file in ["/etc/vservers.conf", self.config_file]:
74             try:
75                 self.config.update(self.__read_config_file(config_file))
76             except IOError, ex:
77                 if ex.errno != errno.ENOENT:
78                     raise
79         self.remove_caps = ~vserverimpl.CAP_SAFE;
80         if vm_id == None:
81             vm_id = int(self.config['S_CONTEXT'])
82         self.ctx = vm_id
83         self.vm_running = vm_running
84
85     def have_limits_changed(self):
86         return self.rlimits_changed
87
88     def set_rlimit_limit(self,type,hard,soft,minimum):
89         """Generic set resource limit function for vserver"""
90         global RLIMITS
91         changed = False
92         try:
93             old_hard, old_soft, old_minimum = self.get_rlimit_limit(type)
94             if old_hard != VC_LIM_KEEP and old_hard <> hard: changed = True
95             if old_soft != VC_LIM_KEEP and old_soft <> soft: changed = True
96             if old_minimum != VC_LIM_KEEP and old_minimum <> minimum: changed = True
97             self.rlimits_changed = self.rlimits_changed or changed 
98         except OSError, e:
99             if self.is_running(): print "Unexpected error with getrlimit for running context %d" % self.ctx
100
101         resource_type = RLIMITS[type]
102         try:
103             ret = vserverimpl.setrlimit(self.ctx,resource_type,hard,soft,minimum)
104         except OSError, e:
105             if self.is_running(): print "Unexpected error with setrlimit for running context %d" % self.ctx
106
107     def set_rlimit_config(self,type,hard,soft,minimum):
108         """Generic set resource limit function for vserver"""
109         resources = {}
110         if hard <> VC_LIM_KEEP:
111             resources["VS_%s_HARD"%type] = hard
112         if soft <> VC_LIM_KEEP:
113             resources["VS_%s_SOFT"%type] = soft
114         if minimum <> VC_LIM_KEEP:
115             resources["VS_%s_MINIMUM"%type] = minimum
116         if len(resources)>0:
117             self.update_resources(resources)
118         self.set_rlimit_limit(type,hard,soft,minimum)
119
120     def get_rlimit_limit(self,type):
121         """Generic get resource configuration function for vserver"""
122         global RLIMITS
123         resource_type = RLIMITS[type]
124         try:
125             ret = vserverimpl.getrlimit(self.ctx,resource_type)
126         except OSError, e:
127             print "Unexpected error with getrlimit for context %d" % self.ctx
128             ret = self.get_rlimit_config(type)
129         return ret
130
131     def get_rlimit_config(self,type):
132         """Generic get resource configuration function for vserver"""
133         hard = int(self.config.get("VS_%s_HARD"%type,VC_LIM_KEEP))
134         soft = int(self.config.get("VS_%s_SOFT"%type,VC_LIM_KEEP))
135         minimum = int(self.config.get("VS_%s_MINIMUM"%type,VC_LIM_KEEP))
136         return (hard,soft,minimum)
137
138     def set_WHITELISTED_config(self,whitelisted):
139         resources = {'VS_WHITELISTED': whitelisted}
140         self.update_resources(resources)
141
142     config_var_re = re.compile(r"^ *([A-Z_]+)=(.*)\n?$", re.MULTILINE)
143
144     def __read_config_file(self, filename):
145
146         f = open(filename, "r")
147         data = f.read()
148         f.close()
149         config = {}
150         for m in self.config_var_re.finditer(data):
151             (key, val) = m.groups()
152             config[key] = val.strip('"')
153         return config
154
155     def __update_config_file(self, filename, newvars):
156
157         # read old file, apply changes
158         f = open(filename, "r")
159         data = f.read()
160         f.close()
161         todo = newvars.copy()
162         changed = False
163         offset = 0
164         for m in self.config_var_re.finditer(data):
165             (key, val) = m.groups()
166             newval = todo.pop(key, None)
167             if newval != None:
168                 data = data[:offset+m.start(2)] + str(newval) + data[offset+m.end(2):]
169                 offset += len(str(newval)) - (m.end(2)-m.start(2))
170                 changed = True
171         for (newkey, newval) in todo.items():
172             data += "%s=%s\n" % (newkey, newval)
173             changed = True
174
175         if not changed:
176             return
177
178         # write new file
179         newfile = filename + ".new"
180         f = open(newfile, "w")
181         f.write(data)
182         f.close()
183
184         # replace old file with new
185         os.rename(newfile, filename)
186
187     def __do_chroot(self):
188         self.cache = True
189         os.chroot(self.dir)
190         os.chdir("/")
191
192     def chroot_call(self, fn, *args):
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         return self.chroot_call(open, filename, mode, bufsize)
299
300     def __do_chcontext(self, state_file):
301
302         if state_file:
303             print >>state_file, "S_CONTEXT=%u" % self.ctx
304             print >>state_file, "S_PROFILE="
305             state_file.close()
306
307         if vserverimpl.chcontext(self.ctx):
308             self.set_resources()
309             vserverimpl.setup_done(self.ctx)
310
311     def __prep(self, runlevel, log):
312
313         """ Perform all the crap that the vserver script does before
314         actually executing the startup scripts. """
315
316         # remove /var/run and /var/lock/subsys files
317         # but don't remove utmp from the top-level /var/run
318         RUNDIR = "/var/run"
319         LOCKDIR = "/var/lock/subsys"
320         filter_fn = lambda fs: filter(lambda f: f != 'utmp', fs)
321         garbage = reduce((lambda (out, ff), (dir, subdirs, files):
322                           (out + map((dir + "/").__add__, ff(files)),
323                            lambda fs: fs)),
324                          list(os.walk(RUNDIR)),
325                          ([], filter_fn))[0]
326         garbage += filter(os.path.isfile, map((LOCKDIR + "/").__add__,
327                                               os.listdir(LOCKDIR)))
328         if False:
329             for f in garbage:
330                 os.unlink(f)
331
332         # set the initial runlevel
333         f = open(RUNDIR + "/utmp", "w")
334         utmp.set_runlevel(f, runlevel)
335         f.close()
336
337         # mount /proc and /dev/pts
338         self.__do_mount("none", "/proc", "proc")
339         # XXX - magic mount options
340         self.__do_mount("none", "/dev/pts", "devpts", 0, "gid=5,mode=0620")
341
342     def __do_mount(self, *mount_args):
343
344         try:
345             mountimpl.mount(*mount_args)
346         except OSError, ex:
347             if ex.errno == errno.EBUSY:
348                 # assume already mounted
349                 return
350             raise ex
351
352     def enter(self):
353         state_file = open("/var/run/vservers/%s.ctx" % self.name, "w")
354         self.__do_chroot()
355         self.__do_chcontext(state_file)
356
357     def start(self, wait, runlevel = 3):
358         self.vm_running = True
359         self.rlimits_changed = False
360
361         child_pid = os.fork()
362         if child_pid == 0:
363             # child process
364             try:
365                 # get a new session
366                 os.setsid()
367
368                 # open state file to record vserver info
369                 state_file = open("/var/run/vservers/%s.ctx" % self.name, "w")
370
371                 # use /dev/null for stdin, /var/log/boot.log for stdout/err
372                 os.close(0)
373                 os.close(1)
374                 os.open("/dev/null", os.O_RDONLY)
375                 self.__do_chroot()
376                 log = open("/var/log/boot.log", "w", 0)
377                 os.dup2(1, 2)
378
379                 print >>log, ("%s: starting the virtual server %s" %
380                               (time.asctime(time.gmtime()), self.name))
381
382                 # perform pre-init cleanup
383                 self.__prep(runlevel, log)
384
385                 # execute each init script in turn
386                 # XXX - we don't support all scripts that vserver script does
387                 self.__do_chcontext(state_file)
388                 for cmd in self.INITSCRIPTS:
389                     try:
390                         print >>log, cmd
391                         # enter vserver context
392                         arg_subst = { 'runlevel': runlevel }
393                         cmd_args = [cmd[0]] + map(lambda x: x % arg_subst,
394                             cmd[1:])
395                         print >>log, "executing '%s'" % " ".join(cmd_args)
396                         os.spawnvp(os.P_NOWAIT,cmd[0],cmd_args)
397                     except:
398                         traceback.print_exc()
399                         os._exit(1)
400
401             # we get here due to an exception in the top-level child process
402             except Exception, ex:
403                 traceback.print_exc()
404             os._exit(0)
405
406         # parent process
407         return child_pid
408
409     def set_resources(self):
410
411         """ Called when vserver context is entered for first time,
412         should be overridden by subclass. """
413
414         pass
415
416     def update_resources(self, resources):
417         self.config.update(resources)
418         if not self.cache:
419             # write new values to configuration file
420             self.__update_config_file(self.config_file, resources)
421
422     def init_disk_info(self):
423         (self.disk_inodes, self.disk_blocks, size) = vduimpl.vdu(self.dir)
424         return size
425
426     def stop(self, signal = signal.SIGKILL):
427         vserverimpl.killall(self.ctx, signal)
428         self.vm_running = False
429         self.rlimits_changed = False
430
431
432 def create(vm_name, static = False, ctor = VServer):
433
434     options = []
435     if static:
436         options += ['--static']
437     runcmd.run('vuseradd', options + [vm_name])
438     vm_id = pwd.getpwnam(vm_name)[2]
439
440     return ctor(vm_name, vm_id)