c57e28197fd102a7547da383dbd9b11b2066fbdc
[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             i += 1
247         while self.config.unset("interfaces/%d/ip" % i):
248             i += 1
249         self.set_ipaddresses(ip_addresses)
250
251     def get_ipaddresses_config(self):
252         i = 0
253         ret = []
254         while True:
255             r = self.config.get("interfaces/%d/ip" % i, '')
256             if r == '':
257                 break
258             ret += [r]
259             i += 1
260         return ",".join(ret)
261
262     def get_ipaddresses(self):
263         # No clean way to do this right now.
264         self.log("Calling Vserver.get_ipaddresses for slice %s" % self.name)
265         return None
266
267     def __do_chroot(self):
268         os.chroot(self.dir)
269         os.chdir("/")
270
271     def chroot_call(self, fn, *args, **kwargs):
272         cwd_fd = os.open(".", os.O_RDONLY)
273         try:
274             root_fd = os.open("/", os.O_RDONLY)
275             try:
276                 self.__do_chroot()
277                 result = fn(*args, **kwargs)
278             finally:
279                 os.fchdir(root_fd)
280                 os.chroot(".")
281                 os.fchdir(cwd_fd)
282                 os.close(root_fd)
283         finally:
284             os.close(cwd_fd)
285         return result
286
287     def set_disklimit(self, block_limit):
288         # block_limit is in kB
289         if block_limit == 0:
290             try:
291                 vserverimpl.unsetdlimit(self.dir, self.ctx)
292             except OSError, e:
293                 self.log("Unexpected error with unsetdlimit for context %d" % self.ctx)
294             return
295
296         if self.vm_running:
297             block_usage = vserverimpl.DLIMIT_KEEP
298             inode_usage = vserverimpl.DLIMIT_KEEP
299         else:
300             # init_disk_info() must have been called to get usage values
301             block_usage = self.disk_blocks
302             inode_usage = self.disk_inodes
303
304         try:
305             vserverimpl.setdlimit(self.dir,
306                                   self.ctx,
307                                   block_usage,
308                                   block_limit,
309                                   inode_usage,
310                                   vserverimpl.DLIMIT_INF,  # inode limit
311                                   2)   # %age reserved for root
312         except OSError, e:
313             self.log("Unexpected error with setdlimit for context %d" % self.ctx)
314
315         self.config.update('dlimits/0/space_total', block_limit)
316
317     def is_running(self):
318         status = subprocess.call(["/usr/sbin/vserver", self.name, "running"], shell=False)
319         return not status
320     
321     def get_disklimit(self):
322         try:
323             (self.disk_blocks, block_limit, self.disk_inodes, inode_limit,
324              reserved) = vserverimpl.getdlimit(self.dir, self.ctx)
325         except OSError, ex:
326             if ex.errno != errno.ESRCH:
327                 raise
328             # get here if no vserver disk limit has been set for xid
329             block_limit = -1
330
331         return block_limit
332
333     def set_sched_config(self, cpu_min, cpu_share):
334         """ Write current CPU scheduler parameters to the vserver
335         configuration file. Currently, 'cpu_min' is not supported. """
336         self.config.update('cgroup/cpu.shares', int(cpu_share) * CPU_SHARE_MULT)
337         if self.is_running():
338             self.set_sched(cpu_min, cpu_share)
339
340     def set_sched(self, cpu_min, cpu_share):
341         """ Update kernel CPU scheduling parameters for this context.
342         Currently, 'cpu_min' is not supported. """
343         try:
344             cgroup = open('/dev/cgroup/%s/cpu.shares' % self.name, 'w')
345             cgroup.write('%s' % (int(cpu_share) * CPU_SHARE_MULT))
346             cgroup.close()
347         except:
348             pass
349
350     def get_sched(self):
351         try:
352             cpu_share = int(int(self.config.get('cgroup/cpu.shares')) / CPU_SHARE_MULT)
353         except:
354             cpu_share = False
355         return (-1, cpu_share)
356
357     def set_bwlimit(self, minrate = bwlimit.bwmin, maxrate = None,
358                     exempt_min = None, exempt_max = None,
359                     share = None, dev = "eth0"):
360
361         if minrate is None:
362             bwlimit.off(self.ctx, dev)
363         else:
364             bwlimit.on(self.ctx, dev, share,
365                        minrate, maxrate, exempt_min, exempt_max)
366
367     def get_bwlimit(self, dev = "eth0"):
368
369         result = bwlimit.get(self.ctx)
370         # result of bwlimit.get is (ctx, share, minrate, maxrate)
371         if result:
372             result = result[1:]
373         return result
374
375     def open(self, filename, mode = "r", bufsize = -1):
376
377         return self.chroot_call(open, filename, mode, bufsize)
378
379     def enter(self):
380         subprocess.call("/usr/sbin/vserver %s enter" % self.name, shell=True)
381
382     # 2010 June 21 - Thierry 
383     # the slice initscript now gets invoked through rc - see sliver_vs.py in nodemanager
384     # and, rc is triggered as part of vserver .. start 
385     # so we don't have to worry about initscripts at all anymore here
386     def start(self, runlevel = 3):
387         if os.fork() != 0:
388             # Parent should just return.
389             self.vm_running = True
390             return
391         else:
392             os.setsid()
393             # first child process: fork again
394             if os.fork() != 0:
395                 os._exit(0)     # Exit parent (the first child) of the second child.
396             # the grandson is the working one
397             os.chdir('/')
398             os.umask(0022)
399             try:
400                 # start the vserver
401                 subprocess.call(["/usr/sbin/vserver",self.name,"start"])
402
403             # we get here due to an exception in the grandson process
404             except Exception, ex:
405                 self.log(traceback.format_exc())
406             os._exit(0)
407
408     def set_resources(self):
409
410         """ Called when vserver context is entered for first time,
411         should be overridden by subclass. """
412
413         pass
414
415     def init_disk_info(self):
416         try:
417             dlimit = vserverimpl.getdlimit(self.dir, self.ctx)
418             self.disk_blocks = dlimit[0]
419             self.disk_inodes = dlimit[2]
420             return self.disk_blocks * 1024
421         except Exception, e:
422             pass
423         cmd = "/usr/sbin/vdu --script --space --inodes --blocksize 1024 --xid %d %s" % (self.ctx, self.dir)
424         p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
425                              stdout=subprocess.PIPE, stderr=subprocess.PIPE,
426                              close_fds=True)
427         p.stdin.close()
428         line = p.stdout.readline()
429         if not line:
430             sys.stderr.write(p.stderr.read())
431         p.stdout.close()
432         p.stderr.close()
433         ret = p.wait()
434
435         (space, inodes) = line.split()
436         self.disk_inodes = int(inodes)
437         self.disk_blocks = int(space)
438
439         return self.disk_blocks * 1024
440
441     def stop(self, signal = signal.SIGKILL):
442         self.vm_running = False
443         subprocess.call("/usr/sbin/vserver %s stop" % self.name, shell=True)
444
445     def setname(self, slice_id):
446         pass
447
448     def getname(self):
449         '''Get vcVHI_CONTEXT field in kernel'''
450         return vserverimpl.getname(self.ctx)
451
452
453 def create(vm_name, static = False, ctor = VServer):
454
455     options = ['vuseradd']
456     if static:
457         options += ['--static']
458     ret = os.spawnvp(os.P_WAIT, 'vuseradd', options + [vm_name])
459     if not os.WIFEXITED(ret) or os.WEXITSTATUS(ret) != 0:
460         out = "system command ('%s') " % options
461         if os.WIFEXITED(ret):
462             out += "failed, rc = %d" % os.WEXITSTATUS(ret)
463         else:
464             out += "killed by signal %d" % os.WTERMSIG(ret)
465         raise SystemError, out
466     vm_id = pwd.getpwnam(vm_name)[2]
467
468     return ctor(vm_name, vm_id)
469
470
471 def close_nonstandard_fds():
472     """Close all open file descriptors other than 0, 1, and 2."""
473     _SC_OPEN_MAX = 4
474     for fd in range(3, os.sysconf(_SC_OPEN_MAX)):
475         try: os.close(fd)
476         except OSError: pass  # most likely an fd that isn't open
477