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