merge trellis branch to trunk.
[util-vserver-pl.git] / python / vserver.py
1 # Copyright 2005 Princeton University
2
3 #$Id: vserver.py,v 1.72 2007/08/02 16:01:59 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 commands
16 import resource
17
18 import vserverimpl
19 import cpulimit, bwlimit
20
21 from vserverimpl import DLIMIT_INF
22 from vserverimpl import VC_LIM_KEEP
23 from vserverimpl import VLIMIT_NSOCK
24 from vserverimpl import VLIMIT_OPENFD
25 from vserverimpl import VLIMIT_ANON
26 from vserverimpl import VLIMIT_SHMEM
27
28 #
29 # these are the flags taken from the kernel linux/vserver/legacy.h
30 #
31 FLAGS_LOCK = 1
32 FLAGS_SCHED = 2  # XXX - defined in util-vserver/src/chcontext.c
33 FLAGS_NPROC = 4
34 FLAGS_PRIVATE = 8
35 FLAGS_INIT = 16
36 FLAGS_HIDEINFO = 32
37 FLAGS_ULIMIT = 64
38 FLAGS_NAMESPACE = 128
39
40 RLIMITS = { "NSOCK": VLIMIT_NSOCK,
41             "OPENFD": VLIMIT_OPENFD,
42             "ANON": VLIMIT_ANON,
43             "SHMEM": VLIMIT_SHMEM}
44
45 CPU_SHARE_MULT = 1024
46
47 # add in the platform supported rlimits
48 for entry in resource.__dict__.keys():
49     if entry.find("RLIMIT_")==0:
50         k = entry[len("RLIMIT_"):]
51         if not RLIMITS.has_key(k):
52             RLIMITS[k]=resource.__dict__[entry]
53         else:
54             print "WARNING: duplicate RLIMITS key %s" % k
55
56 class NoSuchVServer(Exception): pass
57
58 class VServerConfig:
59     def __init__(self, name, directory):
60         self.name = name
61         self.dir = directory
62         self.cache = None
63         if not (os.path.isdir(self.dir) and
64                 os.access(self.dir, os.R_OK | os.W_OK | os.X_OK)):
65             raise NoSuchVServer, "%s does not exist" % self.dir
66
67     def get(self, option, default = None):
68         try:
69             if self.cache:
70                 return self.cache[option]
71             else:
72                 f = open(os.path.join(self.dir, option), "r")
73                 buf = f.read().rstrip()
74                 f.close()
75                 return buf
76         except:
77             if default is not None:
78                 return default
79             else:
80                 raise KeyError, "Key %s is not set for %s" % (option, self.name)
81
82     def update(self, option, value):
83         if self.cache:
84             return
85
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:
101             raise
102
103     def unset(self, option):
104         if self.cache:
105             return
106
107         try:
108             filename = os.path.join(self.dir, option)
109             os.unlink(filename)
110             try:
111                 os.removedirs(os.path.dirname(filename))
112             except:
113                 pass
114             return True
115         except:
116             return False
117
118     def cache_it(self):
119         self.cache = {}
120         def add_to_cache(cache, dirname, fnames):
121             for file in fnames:
122                 full_name = os.path.join(dirname, file)
123                 if os.path.islink(full_name):
124                     fnames.remove(file)
125                 elif (os.path.isfile(full_name) and
126                       os.access(full_name, os.R_OK)):
127                     f = open(full_name, "r")
128                     cache[full_name.replace(os.path.join(self.dir, ''),
129                                             '')] = f.read().rstrip()
130                     f.close()
131         os.path.walk(self.dir, add_to_cache, self.cache)
132
133
134 class VServer:
135
136     INITSCRIPTS = [('/etc/rc.vinit', 'start'),
137                    ('/etc/rc.d/rc', '%(runlevel)d')]
138
139     def __init__(self, name, vm_id = None, vm_running = None, logfile=None):
140
141         self.name = name
142         self.dir = "%s/%s" % (vserverimpl.VSERVER_BASEDIR, name)
143         if not (os.path.isdir(self.dir) and
144                 os.access(self.dir, os.R_OK | os.W_OK | os.X_OK)):
145             raise NoSuchVServer, "no such vserver: " + name
146         self.config = VServerConfig(name, "/etc/vservers/%s" % name)
147         #self.remove_caps = ~vserverimpl.CAP_SAFE;
148         if vm_id == None:
149             vm_id = int(self.config.get('context'))
150         self.ctx = vm_id
151         if vm_running == None:
152             vm_running = self.is_running()
153         self.vm_running = vm_running
154         self.logfile = logfile
155
156     # inspired from nodemanager's logger
157     def log(self,msg):
158         if self.logfile:
159             try:
160                 fd = os.open(self.logfile,os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0600)
161                 if not msg.endswith('\n'): msg += '\n'
162                 os.write(fd, '%s: %s' % (time.asctime(time.gmtime()), msg))
163                 os.close(fd)
164             except:
165                 print '%s: (%s failed to open) %s'%(time.asctime(time.gmtime()),self.logfile,msg)
166
167     def set_rlimit(self, type, hard, soft, min):
168         """Generic set resource limit function for vserver"""
169         global RLIMITS
170         update = False
171
172         if hard <> VC_LIM_KEEP:
173             self.config.update('rlimits/%s.hard' % type.lower(), hard)
174             update = True
175         if soft <> VC_LIM_KEEP:
176             self.config.update('rlimits/%s.soft' % type.lower(), soft)
177             update = True
178         if min <> VC_LIM_KEEP:
179             self.config.update('rlimits/%s.min' % type.lower(), min)
180             update = True
181
182         if self.is_running() and update:
183             resource_type = RLIMITS[type]
184             try:
185                 vserverimpl.setrlimit(self.ctx, resource_type, hard, soft, min)
186             except OSError, e:
187                 self.log("Error: setrlimit(%d, %s, %d, %d, %d): %s"
188                          % (self.ctx, type.lower(), hard, soft, min, e))
189
190         return update
191
192     def get_prefix_from_capabilities(self, capabilities, prefix):
193         split_caps = capabilities.split(',')
194         return ",".join(["%s" % (c) for c in split_caps if c.startswith(prefix.upper()) or c.startswith(prefix.lower())])
195
196     def get_bcaps_from_capabilities(self, capabilities):
197         return self.get_prefix_from_capabilities(capabilities, "cap_")
198
199     def get_ccaps_from_capabilities(self, capabilities):
200         return self.get_prefix_from_capabilities(capabilities, "vxc_")
201
202     def set_capabilities_config(self, capabilities):
203         bcaps = self.get_bcaps_from_capabilities(capabilities)
204         ccaps = self.get_ccaps_from_capabilities(capabilities)
205         if len(bcaps) > 0:
206             bcaps += ","
207         bcaps += "CAP_NET_RAW"
208         self.config.update('bcapabilities', bcaps)
209         self.config.update('ccapabilities', ccaps)
210         ret = vserverimpl.setbcaps(self.ctx, vserverimpl.text2bcaps(bcaps))
211         if ret > 0:
212             return ret
213         return vserverimpl.setccaps(self.ctx, vserverimpl.text2ccaps(ccaps))
214
215     def get_capabilities(self):
216         bcaps = vserverimpl.bcaps2text(vserverimpl.getbcaps(self.ctx))
217         ccaps = vserverimpl.ccaps2text(vserverimpl.getccaps(self.ctx))
218         if bcaps and ccaps:
219             ccaps = "," + ccaps
220         return (bcaps + ccaps)
221  
222     def get_capabilities_config(self):
223         bcaps = self.config.get('bcapabilities', '')
224         ccaps = self.config.get('ccapabilities', '')
225         if bcaps and ccaps:
226             ccaps = "," + ccaps
227         return (bcaps + ccaps)
228
229     def set_ipaddresses(self, addresses):
230         vserverimpl.netremove(self.ctx, "all")
231         for a in addresses.split(","):
232             vserverimpl.netadd(self.ctx, a)
233
234     def set_ipaddresses_config(self, addresses):
235         return # acb
236         i = 0
237         for a in addresses.split(","):
238             self.config.update("interfaces/%d/ip" % i, a)
239             i += 1
240         while self.config.unset("interfaces/%d/ip" % i):
241             i += 1
242         self.set_ipaddresses(addresses)
243
244     def get_ipaddresses_config(self):
245         i = 0
246         ret = []
247         while True:
248             r = self.config.get("interfaces/%d/ip" % i, '')
249             if r == '':
250                 break
251             ret += [r]
252             i += 1
253         return ",".join(ret)
254
255     def get_ipaddresses(self):
256         # No clean way to do this right now.
257         self.log("Calling Vserver.get_ipaddresses for slice %s" % self.name)
258         return None
259
260     def __do_chroot(self):
261         os.chroot(self.dir)
262         os.chdir("/")
263
264     def chroot_call(self, fn, *args):
265         cwd_fd = os.open(".", os.O_RDONLY)
266         try:
267             root_fd = os.open("/", os.O_RDONLY)
268             try:
269                 self.__do_chroot()
270                 result = fn(*args)
271             finally:
272                 os.fchdir(root_fd)
273                 os.chroot(".")
274                 os.fchdir(cwd_fd)
275                 os.close(root_fd)
276         finally:
277             os.close(cwd_fd)
278         return result
279
280     def set_disklimit(self, block_limit):
281         # block_limit is in kB
282         if block_limit == 0:
283             try:
284                 vserverimpl.unsetdlimit(self.dir, self.ctx)
285             except OSError, e:
286                 self.log("Unexpected error with unsetdlimit for context %d" % self.ctx)
287             return
288
289         if self.vm_running:
290             block_usage = vserverimpl.DLIMIT_KEEP
291             inode_usage = vserverimpl.DLIMIT_KEEP
292         else:
293             # init_disk_info() must have been called to get usage values
294             block_usage = self.disk_blocks
295             inode_usage = self.disk_inodes
296
297         try:
298             vserverimpl.setdlimit(self.dir,
299                                   self.ctx,
300                                   block_usage,
301                                   block_limit,
302                                   inode_usage,
303                                   vserverimpl.DLIMIT_INF,  # inode limit
304                                   2)   # %age reserved for root
305         except OSError, e:
306             self.log("Unexpected error with setdlimit for context %d" % self.ctx)
307
308         self.config.update('dlimits/0/space_total', block_limit)
309
310     def is_running(self):
311         status = subprocess.call(["/usr/sbin/vserver", self.name, "running"], shell=False)
312         return not status
313     
314     def get_disklimit(self):
315         try:
316             (self.disk_blocks, block_limit, self.disk_inodes, inode_limit,
317              reserved) = vserverimpl.getdlimit(self.dir, self.ctx)
318         except OSError, ex:
319             if ex.errno != errno.ESRCH:
320                 raise
321             # get here if no vserver disk limit has been set for xid
322             block_limit = -1
323
324         return block_limit
325
326     def set_sched_config(self, cpu_min, cpu_share):
327         """ Write current CPU scheduler parameters to the vserver
328         configuration file. Currently, 'cpu_min' is not supported. """
329         self.config.update('cgroup/cpu.shares', cpu_share * CPU_SHARE_MULT)
330         if self.is_running():
331             self.set_sched(cpu_min, cpu_share)
332
333     def set_sched(self, cpu_min, cpu_share):
334         """ Update kernel CPU scheduling parameters for this context.
335         Currently, 'cpu_min' is not supported. """
336         try:
337             cgroup = open('/dev/cgroup/%s/cpu.shares' % name, 'w')
338             cgroup.write('%s' % (cpu_share * CPU_SHARE_MULT))
339             cgroup.close()
340         except:
341             pass
342
343     def get_sched(self):
344         try:
345             cpu_share = int(int(self.config.get('cgroup/cpu.shares')) / CPU_SHARE_MULT)
346         except:
347             cpu_share = False
348         return (-1, cpu_share)
349
350     def set_bwlimit(self, minrate = bwlimit.bwmin, maxrate = None,
351                     exempt_min = None, exempt_max = None,
352                     share = None, dev = "eth0"):
353
354         if minrate is None:
355             bwlimit.off(self.ctx, dev)
356         else:
357             bwlimit.on(self.ctx, dev, share,
358                        minrate, maxrate, exempt_min, exempt_max)
359
360     def get_bwlimit(self, dev = "eth0"):
361
362         result = bwlimit.get(self.ctx)
363         # result of bwlimit.get is (ctx, share, minrate, maxrate)
364         if result:
365             result = result[1:]
366         return result
367
368     def open(self, filename, mode = "r", bufsize = -1):
369
370         return self.chroot_call(open, filename, mode, bufsize)
371
372     def enter(self):
373         subprocess.call("/usr/sbin/vserver %s enter" % self.name, shell=True)
374
375     def start(self, runlevel = 3):
376         if (os.fork() != 0):
377             # Parent should just return.
378             self.vm_running = True
379             return
380         else:
381             # child process
382             try:
383                 subprocess.call("/usr/sbin/vserver %s start" % self.name, 
384                                 shell=True)
385             # we get here due to an exception in the top-level child process
386             except Exception, ex:
387                 self.log(traceback.format_exc())
388             os._exit(0)
389
390     def set_resources(self):
391
392         """ Called when vserver context is entered for first time,
393         should be overridden by subclass. """
394
395         pass
396
397     def init_disk_info(self):
398         try:
399             dlimit = vserverimpl.getdlimit(self.dir, self.ctx)
400             self.disk_blocks = dlimit[0]
401             self.disk_inodes = dlimit[2]
402             return self.disk_blocks * 1024
403         except Exception, e:
404             pass
405         cmd = "/usr/sbin/vdu --script --space --inodes --blocksize 1024 --xid %d %s" % (self.ctx, self.dir)
406         p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
407                              stdout=subprocess.PIPE, stderr=subprocess.PIPE,
408                              close_fds=True)
409         p.stdin.close()
410         line = p.stdout.readline()
411         if not line:
412             sys.stderr.write(p.stderr.read())
413         p.stdout.close()
414         p.stderr.close()
415         ret = p.wait()
416
417         (space, inodes) = line.split()
418         self.disk_inodes = int(inodes)
419         self.disk_blocks = int(space)
420
421         return self.disk_blocks * 1024
422
423     def stop(self, signal = signal.SIGKILL):
424         self.vm_running = False
425         subprocess.call("/usr/sbin/vserver %s stop" % self.name, shell=True)
426
427     def setname(self, slice_id):
428         pass
429
430     def getname(self):
431         '''Get vcVHI_CONTEXT field in kernel'''
432         return vserverimpl.getname(self.ctx)
433
434
435 def create(vm_name, static = False, ctor = VServer):
436
437     options = ['vuseradd']
438     if static:
439         options += ['--static']
440     ret = os.spawnvp(os.P_WAIT, 'vuseradd', options + [vm_name])
441     if not os.WIFEXITED(ret) or os.WEXITSTATUS(ret) != 0:
442         out = "system command ('%s') " % options
443         if os.WIFEXITED(ret):
444             out += "failed, rc = %d" % os.WEXITSTATUS(ret)
445         else:
446             out += "killed by signal %d" % os.WTERMSIG(ret)
447         raise SystemError, out
448     vm_id = pwd.getpwnam(vm_name)[2]
449
450     return ctor(vm_name, vm_id)
451
452
453 def close_nonstandard_fds():
454     """Close all open file descriptors other than 0, 1, and 2."""
455     _SC_OPEN_MAX = 4
456     for fd in range(3, os.sysconf(_SC_OPEN_MAX)):
457         try: os.close(fd)
458         except OSError: pass  # most likely an fd that isn't open
459