Read default VM id from config file
[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 = None, 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         if vm_id == None:
63             vm_id = int(self.config['S_CONTEXT'])
64         self.ctx = vm_id
65         self.vm_running = vm_running
66
67     config_var_re = re.compile(r"^ *([A-Z_]+)=(.*)\n?$", re.MULTILINE)
68
69     def __read_config_file(self, filename):
70
71         f = open(filename, "r")
72         data = f.read()
73         f.close()
74         config = {}
75         for m in self.config_var_re.finditer(data):
76             (key, val) = m.groups()
77             config[key] = val.strip('"')
78         return config
79
80     def __update_config_file(self, filename, newvars):
81
82         # read old file, apply changes
83         f = open(filename, "r")
84         data = f.read()
85         f.close()
86         todo = newvars.copy()
87         changed = False
88         for m in self.config_var_re.finditer(data):
89             (key, val) = m.groups()
90             newval = todo.pop(key, None)
91             if newval != None:
92                 data = data[:m.start(2)] + str(newval) + data[m.end(2):]
93                 changed = True
94         for (newkey, newval) in todo.items():
95             data += "%s=%s\n" % (newkey, newval)
96             changed = True
97
98         if not changed:
99             return
100
101         # write new file
102         newfile = filename + ".new"
103         f = open(newfile, "w")
104         f.write(data)
105         f.close()
106
107         # 'copy' original file, rename new to original
108         backup = filename + ".old"
109         try:
110             os.unlink(backup)
111         except OSError, ex:
112             if ex.errno != errno.ENOENT:
113                 raise
114         os.link(filename, backup)
115         os.rename(newfile, filename)
116
117     def __do_chroot(self):
118
119         os.chroot(self.dir)
120         os.chdir("/")
121
122     def chroot_call(self, fn, *args):
123
124         cwd_fd = os.open(".", os.O_RDONLY)
125         try:
126             root_fd = os.open("/", os.O_RDONLY)
127             try:
128                 self.__do_chroot()
129                 result = fn(*args)
130             finally:
131                 os.fchdir(root_fd)
132                 os.chroot(".")
133                 os.fchdir(cwd_fd)
134                 os.close(root_fd)
135         finally:
136             os.close(cwd_fd)
137         return result
138
139     def set_disklimit(self, block_limit):
140
141         # block_limit is in kB
142         if block_limit == 0:
143             vserverimpl.unsetdlimit(self.dir, self.ctx)
144             return
145
146         if self.vm_running:
147             block_usage = vserverimpl.DLIMIT_KEEP
148             inode_usage = vserverimpl.DLIMIT_KEEP
149         else:
150             # init_disk_info() must have been called to get usage values
151             block_usage = self.disk_blocks
152             inode_usage = self.disk_inodes
153
154         vserverimpl.setdlimit(self.dir,
155                               self.ctx,
156                               block_usage,
157                               block_limit,
158                               inode_usage,
159                               vserverimpl.DLIMIT_INF,  # inode limit
160                               2)   # %age reserved for root
161
162     def get_disklimit(self):
163
164         try:
165             (self.disk_blocks, block_limit, self.disk_inodes, inode_limit,
166              reserved) = vserverimpl.getdlimit(self.dir, self.ctx)
167         except OSError, ex:
168             if ex.errno != errno.ESRCH:
169                 raise
170             # get here if no vserver disk limit has been set for xid
171             block_limit = -1
172
173         return block_limit
174
175     def set_sched_config(self, cpu_share, sched_flags):
176
177         """ Write current CPU scheduler parameters to the vserver
178         configuration file. This method does not modify the kernel CPU
179         scheduling parameters for this context. """
180
181         if cpu_share == int(self.config.get("CPULIMIT", -1)):
182             return
183         cpu_guaranteed = sched_flags & SCHED_CPU_GUARANTEED
184         cpu_config = { "CPULIMIT": cpu_share, "CPUGUARANTEED": cpu_guaranteed }
185         self.update_resources(cpu_config)
186         if self.vm_running:
187             self.set_sched(cpu_share, sched_flags)
188
189     def set_sched(self, cpu_share, sched_flags = 0):
190
191         """ Update kernel CPU scheduling parameters for this context. """
192
193         vserverimpl.setsched(self.ctx, cpu_share, sched_flags)
194
195     def get_sched(self):
196         # have no way of querying scheduler right now on a per vserver basis
197         return (-1, False)
198
199     def set_memlimit(self, limit):
200         ret = vserverimpl.setrlimit(self.ctx,5,limit)
201         return ret
202
203     def get_memlimit(self):
204         ret = vserverimpl.getrlimit(self.ctx,5)
205         return ret
206     
207     def set_tasklimit(self, limit):
208         ret = vserverimpl.setrlimit(self.ctx,6,limit)
209         return ret
210
211     def get_tasklimit(self):
212         ret = vserverimpl.getrlimit(self.ctx,6)
213         return ret
214
215     def set_bwlimit(self, maxrate, minrate = 1, share = None, dev = "eth0"):
216
217         if maxrate != 0:
218             bwlimit.on(self.ctx, dev, share, minrate, maxrate)
219         else:
220             bwlimit.off(self.ctx, dev)
221
222     def get_bwlimit(self, dev = "eth0"):
223
224         # result of bwlimit.get is (ctx, share, minrate, maxrate)
225         return bwlimit.get(self.ctx)[1:]
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)