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