set vm_running based on is_running, always update scheduler configuration
[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
17 import cpulimit, bwlimit
18
19 from vserverimpl import VS_SCHED_CPU_GUARANTEED as SCHED_CPU_GUARANTEED
20 from vserverimpl import DLIMIT_INF
21 from vserverimpl import VC_LIM_KEEP
22
23 from vserverimpl import RLIMIT_CPU
24 from vserverimpl import RLIMIT_RSS
25 from vserverimpl import RLIMIT_NPROC
26 from vserverimpl import RLIMIT_NOFILE
27 from vserverimpl import RLIMIT_MEMLOCK
28 from vserverimpl import RLIMIT_AS
29 from vserverimpl import RLIMIT_LOCKS
30 from vserverimpl import RLIMIT_SIGPENDING
31 from vserverimpl import RLIMIT_MSGQUEUE
32 from vserverimpl import VLIMIT_NSOCK
33 from vserverimpl import VLIMIT_OPENFD
34 from vserverimpl import VLIMIT_ANON
35 from vserverimpl import VLIMIT_SHMEM
36
37 #
38 # these are the flags taken from the kernel linux/vserver/legacy.h
39 #
40 FLAGS_LOCK = 1
41 FLAGS_SCHED = 2  # XXX - defined in util-vserver/src/chcontext.c
42 FLAGS_NPROC = 4
43 FLAGS_PRIVATE = 8
44 FLAGS_INIT = 16
45 FLAGS_HIDEINFO = 32
46 FLAGS_ULIMIT = 64
47 FLAGS_NAMESPACE = 128
48
49 RLIMITS = {"CPU": RLIMIT_CPU,
50            "RSS": RLIMIT_RSS,
51            "NPROC": RLIMIT_NPROC,
52            "NOFILE": RLIMIT_NOFILE,
53            "MEMLOCK": RLIMIT_MEMLOCK,
54            "AS": RLIMIT_AS,
55            "LOCKS": RLIMIT_LOCKS,
56            "SIGPENDING": RLIMIT_SIGPENDING,
57            "MSGQUEUE": RLIMIT_MSGQUEUE,
58            "NSOCK": VLIMIT_NSOCK,
59            "OPENFD": VLIMIT_OPENFD,
60            "ANON": VLIMIT_ANON,
61            "SHMEM": VLIMIT_SHMEM}
62
63 class NoSuchVServer(Exception): pass
64
65
66 class VServerConfig:
67     mapping = {
68         'S_CONTEXT':            'context',
69         'VS_WHITELISTED':       'whitelisted',
70         'CPULIMIT':             'sched/fill-rate2',
71         'CPUGUARANTEED':        'sched/fill-rate',
72         'VS_DISK_MAX':          'dlimits/0/space_total',
73       }
74
75     def __init__(self, name, directory):
76         global RLIMITS
77         for i in RLIMITS.keys():
78             for j in ('HARD', 'SOFT', 'MINIMUM'):
79                 self.mapping['VS_%s_%s' % (i, j)] = 'rlimits/%s.%s' % (i.lower(), j.replace('MINIMUM', 'min').lower())
80         self.name = name
81         self.dir = directory
82
83     def get(self, option, default = None):
84         try:
85             filename = self.mapping[option]
86             f = open(os.path.join(self.dir, filename), "r")
87             buf = f.readline().rstrip()
88             f.close()
89             return buf
90         except KeyError, e:
91             # No mapping exists for this option
92             raise e
93         except IOError, e:
94             if default is not None:
95                 return default
96             else:
97                 raise KeyError, "Key %s is not set for %s" % (option, self.name)
98
99     def update(self, vars):
100         for (option, value) in vars.iteritems():
101             try:
102                 old_umask = os.umask(0022)
103                 filename = os.path.join(self.dir, self.mapping[option])
104                 try:
105                     os.makedirs(os.path.dirname(filename), 0755)
106                 except:
107                     pass
108                 f = open(filename, 'w')
109                 f.write("%s\n" % value)
110                 f.close()
111                 os.umask(old_umask)
112             except KeyError, e:
113                 raise KeyError, "Don't know how to handle %s, sorry" % option
114
115
116 class VServer:
117
118     INITSCRIPTS = [('/etc/rc.vinit', 'start'),
119                    ('/etc/rc.d/rc', '%(runlevel)d')]
120
121     def __init__(self, name, vm_id = None, vm_running = None):
122
123         self.name = name
124         self.rlimits_changed = False
125         self.dir = "%s/%s" % (vserverimpl.VSERVER_BASEDIR, name)
126         if not (os.path.isdir(self.dir) and
127                 os.access(self.dir, os.R_OK | os.W_OK | os.X_OK)):
128             raise NoSuchVServer, "no such vserver: " + name
129         self.config = VServerConfig(name, "/etc/vservers/%s" % name)
130         self.remove_caps = ~vserverimpl.CAP_SAFE;
131         if vm_id == None:
132             vm_id = int(self.config.get('S_CONTEXT'))
133         self.ctx = vm_id
134         if vm_running == None:
135             vm_running = self.is_running()
136         self.vm_running = vm_running
137
138     def have_limits_changed(self):
139         return self.rlimits_changed
140
141     def set_rlimit_limit(self,type,hard,soft,minimum):
142         """Generic set resource limit function for vserver"""
143         global RLIMITS
144         changed = False
145         try:
146             old_hard, old_soft, old_minimum = self.get_rlimit_limit(type)
147             if old_hard != VC_LIM_KEEP and old_hard <> hard: changed = True
148             if old_soft != VC_LIM_KEEP and old_soft <> soft: changed = True
149             if old_minimum != VC_LIM_KEEP and old_minimum <> minimum: changed = True
150             self.rlimits_changed = self.rlimits_changed or changed 
151         except OSError, e:
152             if self.is_running(): print "Unexpected error with getrlimit for running context %d" % self.ctx
153
154         resource_type = RLIMITS[type]
155         try:
156             ret = vserverimpl.setrlimit(self.ctx,resource_type,hard,soft,minimum)
157         except OSError, e:
158             if self.is_running(): print "Unexpected error with setrlimit for running context %d" % self.ctx
159
160     def set_rlimit_config(self,type,hard,soft,minimum):
161         """Generic set resource limit function for vserver"""
162         resources = {}
163         if hard <> VC_LIM_KEEP:
164             resources["VS_%s_HARD"%type] = hard
165         if soft <> VC_LIM_KEEP:
166             resources["VS_%s_SOFT"%type] = soft
167         if minimum <> VC_LIM_KEEP:
168             resources["VS_%s_MINIMUM"%type] = minimum
169         if len(resources)>0:
170             self.update_resources(resources)
171         self.set_rlimit_limit(type,hard,soft,minimum)
172
173     def get_rlimit_limit(self,type):
174         """Generic get resource configuration function for vserver"""
175         global RLIMITS
176         resource_type = RLIMITS[type]
177         try:
178             ret = vserverimpl.getrlimit(self.ctx,resource_type)
179         except OSError, e:
180             print "Unexpected error with getrlimit for context %d" % self.ctx
181             ret = self.get_rlimit_config(type)
182         return ret
183
184     def get_rlimit_config(self,type):
185         """Generic get resource configuration function for vserver"""
186         hard = int(self.config.get("VS_%s_HARD"%type,VC_LIM_KEEP))
187         soft = int(self.config.get("VS_%s_SOFT"%type,VC_LIM_KEEP))
188         minimum = int(self.config.get("VS_%s_MINIMUM"%type,VC_LIM_KEEP))
189         return (hard,soft,minimum)
190
191     def set_WHITELISTED_config(self,whitelisted):
192         resources = {'VS_WHITELISTED': whitelisted}
193         self.update_resources(resources)
194
195     def __do_chroot(self):
196
197         os.chroot(self.dir)
198         os.chdir("/")
199
200     def chroot_call(self, fn, *args):
201
202         cwd_fd = os.open(".", os.O_RDONLY)
203         try:
204             root_fd = os.open("/", os.O_RDONLY)
205             try:
206                 self.__do_chroot()
207                 result = fn(*args)
208             finally:
209                 os.fchdir(root_fd)
210                 os.chroot(".")
211                 os.fchdir(cwd_fd)
212                 os.close(root_fd)
213         finally:
214             os.close(cwd_fd)
215         return result
216
217     def set_disklimit(self, block_limit):
218         # block_limit is in kB
219         if block_limit == 0:
220             try:
221                 vserverimpl.unsetdlimit(self.dir, self.ctx)
222             except OSError, e:
223                 print "Unexpected error with unsetdlimit for context %d" % self.ctx
224             return
225
226         if self.vm_running:
227             block_usage = vserverimpl.DLIMIT_KEEP
228             inode_usage = vserverimpl.DLIMIT_KEEP
229         else:
230             # init_disk_info() must have been called to get usage values
231             block_usage = self.disk_blocks
232             inode_usage = self.disk_inodes
233
234
235         try:
236             vserverimpl.setdlimit(self.dir,
237                                   self.ctx,
238                                   block_usage,
239                                   block_limit,
240                                   inode_usage,
241                                   vserverimpl.DLIMIT_INF,  # inode limit
242                                   2)   # %age reserved for root
243         except OSError, e:
244             print "Unexpected error with setdlimit for context %d" % self.ctx
245
246
247         resources = {'VS_DISK_MAX': block_limit}
248         self.update_resources(resources)
249
250     def is_running(self):
251         return vserverimpl.isrunning(self.ctx)
252     
253     def get_disklimit(self):
254
255         try:
256             (self.disk_blocks, block_limit, self.disk_inodes, inode_limit,
257              reserved) = vserverimpl.getdlimit(self.dir, self.ctx)
258         except OSError, ex:
259             if ex.errno != errno.ESRCH:
260                 raise
261             # get here if no vserver disk limit has been set for xid
262             block_limit = -1
263
264         return block_limit
265
266     def set_sched_config(self, cpu_share, sched_flags):
267
268         """ Write current CPU scheduler parameters to the vserver
269         configuration file. This method does not modify the kernel CPU
270         scheduling parameters for this context. """
271
272         cpu_guaranteed = sched_flags & SCHED_CPU_GUARANTEED
273         cpu_config = { "CPULIMIT": cpu_share, "CPUGUARANTEED": cpu_guaranteed }
274         self.update_resources(cpu_config)
275         if self.vm_running:
276             self.set_sched(cpu_share, sched_flags)
277
278     def set_sched(self, cpu_share, sched_flags = 0):
279         """ Update kernel CPU scheduling parameters for this context. """
280         vserverimpl.setsched(self.ctx, cpu_share, sched_flags)
281
282     def get_sched(self):
283         # have no way of querying scheduler right now on a per vserver basis
284         return (-1, False)
285
286     def set_bwlimit(self, minrate = bwlimit.bwmin, maxrate = None,
287                     exempt_min = None, exempt_max = None,
288                     share = None, dev = "eth0"):
289
290         if minrate is None:
291             bwlimit.off(self.ctx, dev)
292         else:
293             bwlimit.on(self.ctx, dev, share,
294                        minrate, maxrate, exempt_min, exempt_max)
295
296     def get_bwlimit(self, dev = "eth0"):
297
298         result = bwlimit.get(self.ctx)
299         # result of bwlimit.get is (ctx, share, minrate, maxrate)
300         if result:
301             result = result[1:]
302         return result
303
304     def open(self, filename, mode = "r", bufsize = -1):
305
306         return self.chroot_call(open, filename, mode, bufsize)
307
308     def __do_chcontext(self, state_file):
309
310         if state_file:
311             print >>state_file, "%u" % self.ctx
312             state_file.close()
313
314         if vserverimpl.chcontext(self.ctx):
315             self.set_resources()
316             vserverimpl.setup_done(self.ctx)
317
318     def __prep(self, runlevel, log):
319
320         """ Perform all the crap that the vserver script does before
321         actually executing the startup scripts. """
322
323         # remove /var/run and /var/lock/subsys files
324         # but don't remove utmp from the top-level /var/run
325         RUNDIR = "/var/run"
326         LOCKDIR = "/var/lock/subsys"
327         filter_fn = lambda fs: filter(lambda f: f != 'utmp', fs)
328         garbage = reduce((lambda (out, ff), (dir, subdirs, files):
329                           (out + map((dir + "/").__add__, ff(files)),
330                            lambda fs: fs)),
331                          list(os.walk(RUNDIR)),
332                          ([], filter_fn))[0]
333         garbage += filter(os.path.isfile, map((LOCKDIR + "/").__add__,
334                                               os.listdir(LOCKDIR)))
335         for f in garbage:
336             os.unlink(f)
337
338         # set the initial runlevel
339         f = open(RUNDIR + "/utmp", "w")
340         utmp.set_runlevel(f, runlevel)
341         f.close()
342
343         # mount /proc and /dev/pts
344         self.__do_mount("none", "/proc", "proc")
345         # XXX - magic mount options
346         self.__do_mount("none", "/dev/pts", "devpts", 0, "gid=5,mode=0620")
347
348     def __do_mount(self, *mount_args):
349
350         try:
351             mountimpl.mount(*mount_args)
352         except OSError, ex:
353             if ex.errno == errno.EBUSY:
354                 # assume already mounted
355                 return
356             raise ex
357
358     def enter(self):
359         self.__do_chroot()
360         self.__do_chcontext(None)
361
362     def start(self, wait, runlevel = 3):
363         self.vm_running = True
364         self.rlimits_changed = False
365
366         child_pid = os.fork()
367         if child_pid == 0:
368             # child process
369             try:
370                 # get a new session
371                 os.setsid()
372
373                 # open state file to record vserver info
374                 state_file = open("/var/run/vservers/%s" % self.name, "w")
375
376                 # use /dev/null for stdin, /var/log/boot.log for stdout/err
377                 os.close(0)
378                 os.close(1)
379                 os.open("/dev/null", os.O_RDONLY)
380                 self.__do_chroot()
381                 log = open("/var/log/boot.log", "w", 0)
382                 os.dup2(1, 2)
383
384                 print >>log, ("%s: starting the virtual server %s" %
385                               (time.asctime(time.gmtime()), self.name))
386
387                 # perform pre-init cleanup
388                 self.__prep(runlevel, log)
389
390                 # execute each init script in turn
391                 # XXX - we don't support all scripts that vserver script does
392                 self.__do_chcontext(state_file)
393                 for cmd in self.INITSCRIPTS + [None]:
394                         try:
395                             # enter vserver context
396                             arg_subst = { 'runlevel': runlevel }
397                             cmd_args = [cmd[0]] + map(lambda x: x % arg_subst,
398                                             cmd[1:])
399                             print >>log, "executing '%s'" % " ".join(cmd_args)
400                             os.spawnvp(os.P_WAIT,cmd[0],*cmd_args)
401                         except:
402                                 traceback.print_exc()
403                                 os._exit(1)
404
405             # we get here due to an exception in the top-level child process
406             except Exception, ex:
407                 traceback.print_exc()
408             os._exit(0)
409
410         # parent process
411         return child_pid
412
413     def set_resources(self):
414
415         """ Called when vserver context is entered for first time,
416         should be overridden by subclass. """
417
418         pass
419
420     def update_resources(self, resources):
421
422         self.config.update(resources)
423
424     def init_disk_info(self):
425         cmd = "/usr/sbin/vdu --script --space --inodes --blocksize 1024 --xid %d %s" % (self.ctx, self.dir)
426         (child_stdin, child_stdout, child_stderr) = os.popen3(cmd)
427         child_stdin.close()
428         line = child_stdout.readline()
429         if not line:
430             sys.stderr.write(child_stderr.readline())
431         (space, inodes) = line.split()
432         self.disk_inodes = int(inodes)
433         self.disk_blocks = int(space)
434         #(self.disk_inodes, self.disk_blocks) = vduimpl.vdu(self.dir)
435
436         return self.disk_blocks * 1024
437
438     def stop(self, signal = signal.SIGKILL):
439         vserverimpl.killall(self.ctx, signal)
440         self.vm_running = False
441         self.rlimits_changed = False
442
443
444
445 def create(vm_name, static = False, ctor = VServer):
446
447     options = []
448     if static:
449         options += ['--static']
450     runcmd.run('vuseradd', options + [vm_name])
451     vm_id = pwd.getpwnam(vm_name)[2]
452
453     return ctor(vm_name, vm_id)