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