24704d52d1a0c2b1d50769982fba065b9f256b26
[util-vserver.git] / python / vserver.py
1 # Copyright 2005 Princeton University
2
3 #$Id: vserver.py,v 1.59 2007/07/17 17:56:04 faiyaza Exp $
4
5 import errno
6 import fcntl
7 import os
8 import re
9 import pwd
10 import signal
11 import sys
12 import time
13 import traceback
14
15 import mountimpl
16 import runcmd
17 import utmp
18 import vserverimpl
19 import cpulimit, bwlimit
20
21 from vserverimpl import VS_SCHED_CPU_GUARANTEED as SCHED_CPU_GUARANTEED
22 from vserverimpl import DLIMIT_INF
23 from vserverimpl import VC_LIM_KEEP
24
25 from vserverimpl import RLIMIT_CPU
26 from vserverimpl import RLIMIT_RSS
27 from vserverimpl import RLIMIT_NPROC
28 from vserverimpl import RLIMIT_NOFILE
29 from vserverimpl import RLIMIT_MEMLOCK
30 from vserverimpl import RLIMIT_AS
31 from vserverimpl import RLIMIT_LOCKS
32 from vserverimpl import RLIMIT_SIGPENDING
33 from vserverimpl import RLIMIT_MSGQUEUE
34 from vserverimpl import VLIMIT_NSOCK
35 from vserverimpl import VLIMIT_OPENFD
36 from vserverimpl import VLIMIT_ANON
37 from vserverimpl import VLIMIT_SHMEM
38
39 #
40 # these are the flags taken from the kernel linux/vserver/legacy.h
41 #
42 FLAGS_LOCK = 1
43 FLAGS_SCHED = 2  # XXX - defined in util-vserver/src/chcontext.c
44 FLAGS_NPROC = 4
45 FLAGS_PRIVATE = 8
46 FLAGS_INIT = 16
47 FLAGS_HIDEINFO = 32
48 FLAGS_ULIMIT = 64
49 FLAGS_NAMESPACE = 128
50
51 RLIMITS = {"CPU": RLIMIT_CPU,
52            "RSS": RLIMIT_RSS,
53            "NPROC": RLIMIT_NPROC,
54            "NOFILE": RLIMIT_NOFILE,
55            "MEMLOCK": RLIMIT_MEMLOCK,
56            "AS": RLIMIT_AS,
57            "LOCKS": RLIMIT_LOCKS,
58            "SIGPENDING": RLIMIT_SIGPENDING,
59            "MSGQUEUE": RLIMIT_MSGQUEUE,
60            "NSOCK": VLIMIT_NSOCK,
61            "OPENFD": VLIMIT_OPENFD,
62            "ANON": VLIMIT_ANON,
63            "SHMEM": VLIMIT_SHMEM}
64
65 class NoSuchVServer(Exception): pass
66
67
68 class VServerConfig:
69     def __init__(self, name, directory):
70         self.name = name
71         self.dir = directory
72
73     def get(self, option, default = None):
74         try:
75             f = open(os.path.join(self.dir, option), "r")
76             buf = f.readline().rstrip()
77             f.close()
78             return buf
79         except IOError, e:
80             if default is not None:
81                 return default
82             else:
83                 raise KeyError, "Key %s is not set for %s" % (option, self.name)
84
85     def update(self, option, value):
86         try:
87             old_umask = os.umask(0022)
88             filename = os.path.join(self.dir, option)
89             try:
90                 os.makedirs(os.path.dirname(filename), 0755)
91             except:
92                 pass
93             f = open(filename, 'w')
94             if isinstance(value, list):
95                 f.write("%s\n" % "\n".join(value))
96             else:
97                 f.write("%s\n" % value)
98             f.close()
99             os.umask(old_umask)
100         except KeyError, e:
101             raise KeyError, "Don't know how to handle %s, sorry" % option
102
103
104 class VServer:
105
106     INITSCRIPTS = [('/etc/rc.vinit', 'start'),
107                    ('/etc/rc.d/rc', '%(runlevel)d')]
108
109     def __init__(self, name, vm_id = None, vm_running = None):
110
111         self.name = name
112         self.rlimits_changed = False
113         self.dir = "%s/%s" % (vserverimpl.VSERVER_BASEDIR, name)
114         if not (os.path.isdir(self.dir) and
115                 os.access(self.dir, os.R_OK | os.W_OK | os.X_OK)):
116             raise NoSuchVServer, "no such vserver: " + name
117         self.config = VServerConfig(name, "/etc/vservers/%s" % name)
118         self.remove_caps = ~vserverimpl.CAP_SAFE;
119         if vm_id == None:
120             vm_id = int(self.config.get('context'))
121         self.ctx = vm_id
122         if vm_running == None:
123             vm_running = self.is_running()
124         self.vm_running = vm_running
125
126     def have_limits_changed(self):
127         return self.rlimits_changed
128
129     def set_rlimit_limit(self,type,hard,soft,minimum):
130         """Generic set resource limit function for vserver"""
131         global RLIMITS
132         changed = False
133         try:
134             old_hard, old_soft, old_minimum = self.get_rlimit_limit(type)
135             if old_hard != VC_LIM_KEEP and old_hard <> hard: changed = True
136             if old_soft != VC_LIM_KEEP and old_soft <> soft: changed = True
137             if old_minimum != VC_LIM_KEEP and old_minimum <> minimum: changed = True
138             self.rlimits_changed = self.rlimits_changed or changed 
139         except OSError, e:
140             if self.is_running(): print "Unexpected error with getrlimit for running context %d" % self.ctx
141
142         resource_type = RLIMITS[type]
143         try:
144             ret = vserverimpl.setrlimit(self.ctx,resource_type,hard,soft,minimum)
145         except OSError, e:
146             if self.is_running(): print "Unexpected error with setrlimit for running context %d" % self.ctx
147
148     def set_rlimit_config(self,type,hard,soft,minimum):
149         """Generic set resource limit function for vserver"""
150         if hard <> VC_LIM_KEEP:
151             self.config.update('rlimits/%s.hard' % type.lower(), hard)
152         if soft <> VC_LIM_KEEP:
153             self.config.update('rlimits/%s.soft' % type.lower(), soft)
154         if minimum <> VC_LIM_KEEP:
155             self.config.update('rlimits/%s.min' % type.lower(), minimum)
156         self.set_rlimit_limit(type,hard,soft,minimum)
157
158     def get_rlimit_limit(self,type):
159         """Generic get resource configuration function for vserver"""
160         global RLIMITS
161         resource_type = RLIMITS[type]
162         try:
163             ret = vserverimpl.getrlimit(self.ctx,resource_type)
164         except OSError, e:
165             print "Unexpected error with getrlimit for context %d" % self.ctx
166             ret = self.get_rlimit_config(type)
167         return ret
168
169     def get_rlimit_config(self,type):
170         """Generic get resource configuration function for vserver"""
171         hard = int(self.config.get("rlimits/%s.hard"%type.lower(),VC_LIM_KEEP))
172         soft = int(self.config.get("rlimits/%s.soft"%type.lower(),VC_LIM_KEEP))
173         minimum = int(self.config.get("rlimits/%s.min"%type.lower(),VC_LIM_KEEP))
174         return (hard,soft,minimum)
175
176     def set_WHITELISTED_config(self,whitelisted):
177         self.config.update('whitelisted', whitelisted)
178
179     def set_capabilities(self, capabilities):
180         return vserverimpl.setbcaps(self.ctx, vserverimpl.text2bcaps(capabilities))
181
182     def set_capabilities_config(self, capabilities):
183         self.config.update('bcapabilities', capabilities)
184         self.set_capabilities(capabilities)
185
186     def get_capabilities(self):
187         return vserverimpl.bcaps2text(vserverimpl.getbcaps(self.ctx))
188  
189     def get_capabilities_config(self):
190         return self.config.get('bcapabilities', '')
191
192     def __do_chroot(self):
193
194         os.chroot(self.dir)
195         os.chdir("/")
196
197     def chroot_call(self, fn, *args):
198
199         cwd_fd = os.open(".", os.O_RDONLY)
200         try:
201             root_fd = os.open("/", os.O_RDONLY)
202             try:
203                 self.__do_chroot()
204                 result = fn(*args)
205             finally:
206                 os.fchdir(root_fd)
207                 os.chroot(".")
208                 os.fchdir(cwd_fd)
209                 os.close(root_fd)
210         finally:
211             os.close(cwd_fd)
212         return result
213
214     def set_disklimit(self, block_limit):
215         # block_limit is in kB
216         if block_limit == 0:
217             try:
218                 vserverimpl.unsetdlimit(self.dir, self.ctx)
219             except OSError, e:
220                 print "Unexpected error with unsetdlimit for context %d" % self.ctx
221             return
222
223         if self.vm_running:
224             block_usage = vserverimpl.DLIMIT_KEEP
225             inode_usage = vserverimpl.DLIMIT_KEEP
226         else:
227             # init_disk_info() must have been called to get usage values
228             block_usage = self.disk_blocks
229             inode_usage = self.disk_inodes
230
231
232         try:
233             vserverimpl.setdlimit(self.dir,
234                                   self.ctx,
235                                   block_usage,
236                                   block_limit,
237                                   inode_usage,
238                                   vserverimpl.DLIMIT_INF,  # inode limit
239                                   2)   # %age reserved for root
240         except OSError, e:
241             print "Unexpected error with setdlimit for context %d" % self.ctx
242
243
244         self.config.update('dlimits/0/space_total', block_limit)
245
246     def is_running(self):
247         return vserverimpl.isrunning(self.ctx)
248     
249     def get_disklimit(self):
250
251         try:
252             (self.disk_blocks, block_limit, self.disk_inodes, inode_limit,
253              reserved) = vserverimpl.getdlimit(self.dir, self.ctx)
254         except OSError, ex:
255             if ex.errno != errno.ESRCH:
256                 raise
257             # get here if no vserver disk limit has been set for xid
258             block_limit = -1
259
260         return block_limit
261
262     def set_sched_config(self, cpu_share, sched_flags):
263
264         """ Write current CPU scheduler parameters to the vserver
265         configuration file. This method does not modify the kernel CPU
266         scheduling parameters for this context. """
267
268         if sched_flags & SCHED_CPU_GUARANTEED:
269             cpu_guaranteed = cpu_share
270         else:
271             cpu_guaranteed = 0
272         self.config.update('sched/fill-rate2', cpu_share)
273         self.config.update('sched/fill-rate', cpu_guaranteed)
274
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, vserverimpl.text2bcaps(self.get_capabilities_config())):
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 init_disk_info(self):
421         cmd = "/usr/sbin/vdu --script --space --inodes --blocksize 1024 --xid %d %s" % (self.ctx, self.dir)
422         (child_stdin, child_stdout, child_stderr) = os.popen3(cmd)
423         child_stdin.close()
424         line = child_stdout.readline()
425         if not line:
426             sys.stderr.write(child_stderr.readline())
427         child_stdout.close()
428         child_stderr.close()
429         (space, inodes) = line.split()
430         self.disk_inodes = int(inodes)
431         self.disk_blocks = int(space)
432         #(self.disk_inodes, self.disk_blocks) = vduimpl.vdu(self.dir)
433
434         return self.disk_blocks * 1024
435
436     def stop(self, signal = signal.SIGKILL):
437         vserverimpl.killall(self.ctx, signal)
438         self.vm_running = False
439         self.rlimits_changed = False
440
441
442
443 def create(vm_name, static = False, ctor = VServer):
444
445     options = []
446     if static:
447         options += ['--static']
448     runcmd.run('vuseradd', options + [vm_name])
449     vm_id = pwd.getpwnam(vm_name)[2]
450
451     return ctor(vm_name, vm_id)