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