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