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