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