Remove any notions of rspecs from util-vserver, fix handling of resources
[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 VServer:
38
39     INITSCRIPTS = [('/etc/rc.vinit', 'start'),
40                    ('/etc/rc.d/rc', '%(runlevel)d')]
41
42     def __init__(self, name, vm_id, vm_running = False):
43
44         self.name = name
45         self.config_file = "/etc/vservers/%s.conf" % name
46         self.dir = "%s/%s" % (vserverimpl.VSERVER_BASEDIR, name)
47         if not (os.path.isdir(self.dir) and
48                 os.access(self.dir, os.R_OK | os.W_OK | os.X_OK)):
49             raise Exception, "no such vserver: " + name
50         self.config = {}
51         for config_file in ["/etc/vservers.conf", self.config_file]:
52             try:
53                 self.config.update(self.__read_config_file(config_file))
54             except IOError, ex:
55                 if ex.errno != errno.ENOENT:
56                     raise
57         self.remove_caps = ~vserverimpl.CAP_SAFE;
58         self.ctx = vm_id
59         self.vm_running = vm_running
60
61     config_var_re = re.compile(r"^ *([A-Z_]+)=(.*)\n?$", re.MULTILINE)
62
63     def __read_config_file(self, filename):
64
65         f = open(filename, "r")
66         data = f.read()
67         f.close()
68         config = {}
69         for m in self.config_var_re.finditer(data):
70             (key, val) = m.groups()
71             config[key] = val.strip('"')
72         return config
73
74     def __update_config_file(self, filename, newvars):
75
76         # read old file, apply changes
77         f = open(filename, "r")
78         data = f.read()
79         f.close()
80         todo = newvars.copy()
81         changed = False
82         for m in self.config_var_re.finditer(data):
83             (key, val) = m.groups()
84             newval = todo.pop(key, None)
85             if newval != None:
86                 data = data[:m.start(2)] + str(newval) + data[m.end(2):]
87                 changed = True
88         for (newkey, newval) in todo.items():
89             data += "%s=%s\n" % (newkey, newval)
90             changed = True
91
92         if not changed:
93             return
94
95         # write new file
96         newfile = filename + ".new"
97         f = open(newfile, "w")
98         f.write(data)
99         f.close()
100
101         # 'copy' original file, rename new to original
102         backup = filename + ".old"
103         try:
104             os.unlink(backup)
105         except OSError, ex:
106             if ex.errno != errno.ENOENT:
107                 raise
108         os.link(filename, backup)
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(self, cpu_share, sched_flags = 0):
170
171         if cpu_share == int(self.config.get("CPULIMIT", -1)):
172             return
173         cpu_guaranteed = sched_flags & SCHED_CPU_GUARANTEED
174         #cpu_guaranteed = int(self.resources.get("nm_sched_flags",
175         #                                        None) == "guaranteed")
176         cpu_config = { "CPULIMIT": cpu_share, "CPUGUARANTEED": cpu_guaranteed }
177         self.__update_config_file(self.config_file, cpu_config)
178         if self.vm_running:
179             vserverimpl.setsched(self.ctx, cpu_share, sched_flags)
180
181     def get_sched(self):
182         # have no way of querying scheduler right now on a per vserver basis
183         return (-1, False)
184
185     def set_memlimit(self, limit):
186         ret = vserverimpl.setrlimit(self.ctx,5,limit)
187         return ret
188
189     def get_memlimit(self):
190         ret = vserverimpl.getrlimit(self.ctx,5)
191         return ret
192     
193     def set_tasklimit(self, limit):
194         ret = vserverimpl.setrlimit(self.ctx,6,limit)
195         return ret
196
197     def get_tasklimit(self):
198         ret = vserverimpl.getrlimit(self.ctx,6)
199         return ret
200
201     def set_bwlimit(self, maxrate, minrate = 1, share = None, dev = "eth0"):
202
203         if maxrate != 0:
204             bwlimit.on(self.ctx, dev, share, minrate, maxrate)
205         else:
206             bwlimit.off(self.ctx, dev)
207
208     def get_bwlimit(self, dev = "eth0"):
209
210         # result of bwlimit.get is (ctx, share, minrate, maxrate)
211         return bwlimit.get(self.ctx)[1:]
212
213     def open(self, filename, mode = "r", bufsize = -1):
214
215         return self.chroot_call(open, filename, mode, bufsize)
216
217     def __do_chcontext(self, state_file):
218
219         if state_file:
220             print >>state_file, "S_CONTEXT=%u" % self.ctx
221             print >>state_file, "S_PROFILE="
222             state_file.close()
223
224         if vserverimpl.chcontext(self.ctx):
225             self.set_resources()
226             vserverimpl.setup_done(self.ctx)
227
228     def __prep(self, runlevel, log):
229
230         """ Perform all the crap that the vserver script does before
231         actually executing the startup scripts. """
232
233         # remove /var/run and /var/lock/subsys files
234         # but don't remove utmp from the top-level /var/run
235         RUNDIR = "/var/run"
236         LOCKDIR = "/var/lock/subsys"
237         filter_fn = lambda fs: filter(lambda f: f != 'utmp', fs)
238         garbage = reduce((lambda (out, ff), (dir, subdirs, files):
239                           (out + map((dir + "/").__add__, ff(files)),
240                            lambda fs: fs)),
241                          list(os.walk(RUNDIR)),
242                          ([], filter_fn))[0]
243         garbage += filter(os.path.isfile, map((LOCKDIR + "/").__add__,
244                                               os.listdir(LOCKDIR)))
245         for f in garbage:
246             os.unlink(f)
247
248         # set the initial runlevel
249         f = open(RUNDIR + "/utmp", "w")
250         utmp.set_runlevel(f, runlevel)
251         f.close()
252
253         # mount /proc and /dev/pts
254         self.__do_mount("none", "/proc", "proc")
255         # XXX - magic mount options
256         self.__do_mount("none", "/dev/pts", "devpts", 0, "gid=5,mode=0620")
257
258     def __do_mount(self, *mount_args):
259
260         try:
261             mountimpl.mount(*mount_args)
262         except OSError, ex:
263             if ex.errno == errno.EBUSY:
264                 # assume already mounted
265                 return
266             raise ex
267
268     def enter(self):
269
270         state_file = open("/var/run/vservers/%s.ctx" % self.name, "w")
271         self.__do_chroot()
272         self.__do_chcontext(state_file)
273
274     def start(self, wait, runlevel = 3):
275
276         self.vm_running = True
277
278         child_pid = os.fork()
279         if child_pid == 0:
280             # child process
281             try:
282                 # get a new session
283                 os.setsid()
284
285                 # open state file to record vserver info
286                 state_file = open("/var/run/vservers/%s.ctx" % self.name, "w")
287
288                 # use /dev/null for stdin, /var/log/boot.log for stdout/err
289                 os.close(0)
290                 os.close(1)
291                 os.open("/dev/null", os.O_RDONLY)
292                 self.__do_chroot()
293                 log = open("/var/log/boot.log", "w", 0)
294                 os.dup2(1, 2)
295
296                 print >>log, ("%s: starting the virtual server %s" %
297                               (time.asctime(time.gmtime()), self.name))
298
299                 # perform pre-init cleanup
300                 self.__prep(runlevel, log)
301
302                 # execute each init script in turn
303                 # XXX - we don't support all scripts that vserver script does
304                 cmd_pid = 0
305                 first_child = True
306                 for cmd in self.INITSCRIPTS + [None]:
307                     # wait for previous command to terminate, unless it
308                     # is the last one and the caller has specified to wait
309                     if cmd_pid and (cmd != None or wait):
310                         try:
311                             os.waitpid(cmd_pid, 0)
312                         except:
313                             print >>log, "error waiting for %s:" % cmd_pid
314                             traceback.print_exc()
315
316                     # end of list
317                     if cmd == None:
318                         os._exit(0)
319
320                     # fork and exec next command
321                     cmd_pid = os.fork()
322                     if cmd_pid == 0:
323                         try:
324                             # enter vserver context
325                             self.__do_chcontext(state_file)
326                             arg_subst = { 'runlevel': runlevel }
327                             cmd_args = [cmd[0]] + map(lambda x: x % arg_subst,
328                                                       cmd[1:])
329                             print >>log, "executing '%s'" % " ".join(cmd_args)
330                             os.execl(cmd[0], *cmd_args)
331                         except:
332                             traceback.print_exc()
333                             os._exit(1)
334                     else:
335                         # don't want to write state_file multiple times
336                         state_file = None
337
338             # we get here due to an exception in the top-level child process
339             except Exception, ex:
340                 traceback.print_exc()
341             os._exit(0)
342
343         # parent process
344         return child_pid
345
346     def set_resources(self):
347
348         """ Called when vserver context is entered for first time,
349         should be overridden by subclass. """
350
351         pass
352
353     def update_resources(self, resources):
354
355         self.config.update(resources)
356
357         # write new values to configuration file
358         self.__update_config_file(self.config_file, resources)
359
360     def init_disk_info(self):
361
362         (self.disk_inodes, self.disk_blocks, size) = vduimpl.vdu(self.dir)
363
364         return size
365
366     def stop(self, signal = signal.SIGKILL):
367
368         vserverimpl.killall(self.ctx, signal)
369         self.vm_running = False
370
371
372
373 def create(vm_name, static = False, ctor = VServer):
374
375     options = []
376     if static:
377         options += ['--static']
378     runcmd.run('vuseradd', options + [vm_name])
379     vm_id = pwd.getpwnam(vm_name)[2]
380
381     return ctor(vm_name, vm_id)