simpler yet, no need to trigger rc at all now that we use vserver .. start
[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     def __init__(self, name, vm_id = None, vm_running = None, logfile=None):
137
138         self.name = name
139         self.dir = "%s/%s" % (vserverimpl.VSERVER_BASEDIR, name)
140         if not (os.path.isdir(self.dir) and
141                 os.access(self.dir, os.R_OK | os.W_OK | os.X_OK)):
142             raise NoSuchVServer, "no such vserver: " + name
143         self.config = VServerConfig(name, "/etc/vservers/%s" % name)
144         #self.remove_caps = ~vserverimpl.CAP_SAFE;
145         if vm_id == None:
146             vm_id = int(self.config.get('context'))
147         self.ctx = vm_id
148         if vm_running == None:
149             vm_running = self.is_running()
150         self.vm_running = vm_running
151         self.logfile = logfile
152
153     # inspired from nodemanager's logger
154     def log_in_file (self, fd, msg):
155         if not msg: msg="\n"
156         if not msg.endswith('\n'): msg += '\n'
157         os.write(fd, '%s: %s' % (time.asctime(time.gmtime()), msg))
158
159     def log(self,msg):
160         if self.logfile:
161             try:
162                 fd = os.open(self.logfile,os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0600)
163                 self.log_in_file(fd,msg)
164                 os.close(fd)
165             except:
166                 print '%s: (%s failed to open) %s'%(time.asctime(time.gmtime()),self.logfile,msg)
167
168     def set_rlimit(self, type, hard, soft, min):
169         """Generic set resource limit function for vserver"""
170         global RLIMITS
171         update = False
172
173         if hard <> VC_LIM_KEEP:
174             self.config.update('rlimits/%s.hard' % type.lower(), hard)
175             update = True
176         if soft <> VC_LIM_KEEP:
177             self.config.update('rlimits/%s.soft' % type.lower(), soft)
178             update = True
179         if min <> VC_LIM_KEEP:
180             self.config.update('rlimits/%s.min' % type.lower(), min)
181             update = True
182
183         if self.is_running() and update:
184             resource_type = RLIMITS[type]
185             try:
186                 vserverimpl.setrlimit(self.ctx, resource_type, hard, soft, min)
187             except OSError, e:
188                 self.log("Error: setrlimit(%d, %s, %d, %d, %d): %s"
189                          % (self.ctx, type.lower(), hard, soft, min, e))
190
191         return update
192
193     def get_prefix_from_capabilities(self, capabilities, prefix):
194         split_caps = capabilities.split(',')
195         return ",".join(["%s" % (c) for c in split_caps if c.startswith(prefix.upper()) or c.startswith(prefix.lower())])
196
197     def get_bcaps_from_capabilities(self, capabilities):
198         return self.get_prefix_from_capabilities(capabilities, "cap_")
199
200     def get_ccaps_from_capabilities(self, capabilities):
201         return self.get_prefix_from_capabilities(capabilities, "vxc_")
202
203     def set_capabilities_config(self, capabilities):
204         bcaps = self.get_bcaps_from_capabilities(capabilities)
205         ccaps = self.get_ccaps_from_capabilities(capabilities)
206         if len(bcaps) > 0:
207             bcaps += ","
208         bcaps += "CAP_NET_RAW"
209         self.config.update('bcapabilities', bcaps)
210         self.config.update('ccapabilities', ccaps)
211         ret = vserverimpl.setbcaps(self.ctx, vserverimpl.text2bcaps(bcaps))
212         if ret > 0:
213             return ret
214         return vserverimpl.setccaps(self.ctx, vserverimpl.text2ccaps(ccaps))
215
216     def get_capabilities(self):
217         bcaps = vserverimpl.bcaps2text(vserverimpl.getbcaps(self.ctx))
218         ccaps = vserverimpl.ccaps2text(vserverimpl.getccaps(self.ctx))
219         if bcaps and ccaps:
220             ccaps = "," + ccaps
221         return (bcaps + ccaps)
222  
223     def get_capabilities_config(self):
224         bcaps = self.config.get('bcapabilities', '')
225         ccaps = self.config.get('ccapabilities', '')
226         if bcaps and ccaps:
227             ccaps = "," + ccaps
228         return (bcaps + ccaps)
229
230     def set_ipaddresses(self, addresses):
231         vserverimpl.netremove(self.ctx, "all")
232         for a in addresses.split(","):
233             vserverimpl.netadd(self.ctx, a)
234
235     def set_ipaddresses_config(self, addresses):
236         return # acb
237         i = 0
238         for a in addresses.split(","):
239             self.config.update("interfaces/%d/ip" % i, a)
240             i += 1
241         while self.config.unset("interfaces/%d/ip" % i):
242             i += 1
243         self.set_ipaddresses(addresses)
244
245     def get_ipaddresses_config(self):
246         i = 0
247         ret = []
248         while True:
249             r = self.config.get("interfaces/%d/ip" % i, '')
250             if r == '':
251                 break
252             ret += [r]
253             i += 1
254         return ",".join(ret)
255
256     def get_ipaddresses(self):
257         # No clean way to do this right now.
258         self.log("Calling Vserver.get_ipaddresses for slice %s" % self.name)
259         return None
260
261     def __do_chroot(self):
262         os.chroot(self.dir)
263         os.chdir("/")
264
265     def chroot_call(self, fn, *args, **kwargs):
266         cwd_fd = os.open(".", os.O_RDONLY)
267         try:
268             root_fd = os.open("/", os.O_RDONLY)
269             try:
270                 self.__do_chroot()
271                 result = fn(*args, **kwargs)
272             finally:
273                 os.fchdir(root_fd)
274                 os.chroot(".")
275                 os.fchdir(cwd_fd)
276                 os.close(root_fd)
277         finally:
278             os.close(cwd_fd)
279         return result
280
281     def set_disklimit(self, block_limit):
282         # block_limit is in kB
283         if block_limit == 0:
284             try:
285                 vserverimpl.unsetdlimit(self.dir, self.ctx)
286             except OSError, e:
287                 self.log("Unexpected error with unsetdlimit for context %d" % self.ctx)
288             return
289
290         if self.vm_running:
291             block_usage = vserverimpl.DLIMIT_KEEP
292             inode_usage = vserverimpl.DLIMIT_KEEP
293         else:
294             # init_disk_info() must have been called to get usage values
295             block_usage = self.disk_blocks
296             inode_usage = self.disk_inodes
297
298         try:
299             vserverimpl.setdlimit(self.dir,
300                                   self.ctx,
301                                   block_usage,
302                                   block_limit,
303                                   inode_usage,
304                                   vserverimpl.DLIMIT_INF,  # inode limit
305                                   2)   # %age reserved for root
306         except OSError, e:
307             self.log("Unexpected error with setdlimit for context %d" % self.ctx)
308
309         self.config.update('dlimits/0/space_total', block_limit)
310
311     def is_running(self):
312         status = subprocess.call(["/usr/sbin/vserver", self.name, "running"], shell=False)
313         return not status
314     
315     def get_disklimit(self):
316         try:
317             (self.disk_blocks, block_limit, self.disk_inodes, inode_limit,
318              reserved) = vserverimpl.getdlimit(self.dir, self.ctx)
319         except OSError, ex:
320             if ex.errno != errno.ESRCH:
321                 raise
322             # get here if no vserver disk limit has been set for xid
323             block_limit = -1
324
325         return block_limit
326
327     def set_sched_config(self, cpu_min, cpu_share):
328         """ Write current CPU scheduler parameters to the vserver
329         configuration file. Currently, 'cpu_min' is not supported. """
330         self.config.update('cgroup/cpu.shares', cpu_share * CPU_SHARE_MULT)
331         if self.is_running():
332             self.set_sched(cpu_min, cpu_share)
333
334     def set_sched(self, cpu_min, cpu_share):
335         """ Update kernel CPU scheduling parameters for this context.
336         Currently, 'cpu_min' is not supported. """
337         try:
338             cgroup = open('/dev/cgroup/%s/cpu.shares' % name, 'w')
339             cgroup.write('%s' % (cpu_share * CPU_SHARE_MULT))
340             cgroup.close()
341         except:
342             pass
343
344     def get_sched(self):
345         try:
346             cpu_share = int(int(self.config.get('cgroup/cpu.shares')) / CPU_SHARE_MULT)
347         except:
348             cpu_share = False
349         return (-1, cpu_share)
350
351     def set_bwlimit(self, minrate = bwlimit.bwmin, maxrate = None,
352                     exempt_min = None, exempt_max = None,
353                     share = None, dev = "eth0"):
354
355         if minrate is None:
356             bwlimit.off(self.ctx, dev)
357         else:
358             bwlimit.on(self.ctx, dev, share,
359                        minrate, maxrate, exempt_min, exempt_max)
360
361     def get_bwlimit(self, dev = "eth0"):
362
363         result = bwlimit.get(self.ctx)
364         # result of bwlimit.get is (ctx, share, minrate, maxrate)
365         if result:
366             result = result[1:]
367         return result
368
369     def open(self, filename, mode = "r", bufsize = -1):
370
371         return self.chroot_call(open, filename, mode, bufsize)
372
373     def enter(self):
374         subprocess.call("/usr/sbin/vserver %s enter" % self.name, shell=True)
375
376     # 2010 June 21 - Thierry 
377     # the slice initscript now gets invoked through rc - see sliver_vs.py in nodemanager
378     # and, rc is triggered as part of vserver .. start 
379     # so we don't have to worry about initscripts at all anymore here
380     def start(self, runlevel = 3):
381         if os.fork() != 0:
382             # Parent should just return.
383             self.vm_running = True
384             return
385         else:
386             os.setsid()
387             # first child process: fork again
388             if os.fork() != 0:
389                 os._exit(0)     # Exit parent (the first child) of the second child.
390             # the grandson is the working one
391             os.chdir('/')
392             os.umask(0)
393             try:
394                 # start the vserver
395                 subprocess.call(["/usr/sbin/vserver",self.name,"start"])
396
397             # we get here due to an exception in the grandson process
398             except Exception, ex:
399                 self.log(traceback.format_exc())
400             os._exit(0)
401
402     def set_resources(self):
403
404         """ Called when vserver context is entered for first time,
405         should be overridden by subclass. """
406
407         pass
408
409     def init_disk_info(self):
410         try:
411             dlimit = vserverimpl.getdlimit(self.dir, self.ctx)
412             self.disk_blocks = dlimit[0]
413             self.disk_inodes = dlimit[2]
414             return self.disk_blocks * 1024
415         except Exception, e:
416             pass
417         cmd = "/usr/sbin/vdu --script --space --inodes --blocksize 1024 --xid %d %s" % (self.ctx, self.dir)
418         p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
419                              stdout=subprocess.PIPE, stderr=subprocess.PIPE,
420                              close_fds=True)
421         p.stdin.close()
422         line = p.stdout.readline()
423         if not line:
424             sys.stderr.write(p.stderr.read())
425         p.stdout.close()
426         p.stderr.close()
427         ret = p.wait()
428
429         (space, inodes) = line.split()
430         self.disk_inodes = int(inodes)
431         self.disk_blocks = int(space)
432
433         return self.disk_blocks * 1024
434
435     def stop(self, signal = signal.SIGKILL):
436         self.vm_running = False
437         subprocess.call("/usr/sbin/vserver %s stop" % self.name, shell=True)
438
439     def setname(self, slice_id):
440         pass
441
442     def getname(self):
443         '''Get vcVHI_CONTEXT field in kernel'''
444         return vserverimpl.getname(self.ctx)
445
446
447 def create(vm_name, static = False, ctor = VServer):
448
449     options = ['vuseradd']
450     if static:
451         options += ['--static']
452     ret = os.spawnvp(os.P_WAIT, 'vuseradd', options + [vm_name])
453     if not os.WIFEXITED(ret) or os.WEXITSTATUS(ret) != 0:
454         out = "system command ('%s') " % options
455         if os.WIFEXITED(ret):
456             out += "failed, rc = %d" % os.WEXITSTATUS(ret)
457         else:
458             out += "killed by signal %d" % os.WTERMSIG(ret)
459         raise SystemError, out
460     vm_id = pwd.getpwnam(vm_name)[2]
461
462     return ctor(vm_name, vm_id)
463
464
465 def close_nonstandard_fds():
466     """Close all open file descriptors other than 0, 1, and 2."""
467     _SC_OPEN_MAX = 4
468     for fd in range(3, os.sysconf(_SC_OPEN_MAX)):
469         try: os.close(fd)
470         except OSError: pass  # most likely an fd that isn't open
471