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