merged from 0.4
[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 resource
16
17 import vserverimpl
18 import cpulimit, bwlimit
19
20 from vserverimpl import DLIMIT_INF
21 from vserverimpl import VC_LIM_KEEP
22 from vserverimpl import VLIMIT_NSOCK
23 from vserverimpl import VLIMIT_OPENFD
24 from vserverimpl import VLIMIT_ANON
25 from vserverimpl import VLIMIT_SHMEM
26
27 #
28 # these are the flags taken from the kernel linux/vserver/legacy.h
29 #
30 FLAGS_LOCK = 1
31 FLAGS_SCHED = 2  # XXX - defined in util-vserver/src/chcontext.c
32 FLAGS_NPROC = 4
33 FLAGS_PRIVATE = 8
34 FLAGS_INIT = 16
35 FLAGS_HIDEINFO = 32
36 FLAGS_ULIMIT = 64
37 FLAGS_NAMESPACE = 128
38
39 RLIMITS = { "NSOCK": VLIMIT_NSOCK,
40             "OPENFD": VLIMIT_OPENFD,
41             "ANON": VLIMIT_ANON,
42             "SHMEM": VLIMIT_SHMEM}
43
44 # add in the platform supported rlimits
45 for entry in resource.__dict__.keys():
46     if entry.find("RLIMIT_")==0:
47         k = entry[len("RLIMIT_"):]
48         if not RLIMITS.has_key(k):
49             RLIMITS[k]=resource.__dict__[entry]
50         else:
51             print "WARNING: duplicate RLIMITS key %s" % k
52
53 class NoSuchVServer(Exception): pass
54
55
56 class VServerConfig:
57     def __init__(self, name, directory):
58         self.name = name
59         self.dir = directory
60         self.cache = None
61         if not (os.path.isdir(self.dir) and
62                 os.access(self.dir, os.R_OK | os.W_OK | os.X_OK)):
63             raise NoSuchVServer, "%s does not exist" % self.dir
64
65     def get(self, option, default = None):
66         try:
67             if self.cache:
68                 return self.cache[option]
69             else:
70                 f = open(os.path.join(self.dir, option), "r")
71                 buf = f.read().rstrip()
72                 f.close()
73                 return buf
74         except:
75             if default is not None:
76                 return default
77             else:
78                 raise KeyError, "Key %s is not set for %s" % (option, self.name)
79
80     def update(self, option, value):
81         if self.cache:
82             return
83
84         try:
85             old_umask = os.umask(0022)
86             filename = os.path.join(self.dir, option)
87             try:
88                 os.makedirs(os.path.dirname(filename), 0755)
89             except:
90                 pass
91             f = open(filename, 'w')
92             if isinstance(value, list):
93                 f.write("%s\n" % "\n".join(value))
94             else:
95                 f.write("%s\n" % value)
96             f.close()
97             os.umask(old_umask)
98         except:
99             raise
100
101     def unset(self, option):
102         if self.cache:
103             return
104
105         try:
106             filename = os.path.join(self.dir, option)
107             os.unlink(filename)
108             try:
109                 os.removedirs(os.path.dirname(filename))
110             except:
111                 pass
112             return True
113         except:
114             return False
115
116     def cache_it(self):
117         self.cache = {}
118         def add_to_cache(cache, dirname, fnames):
119             for file in fnames:
120                 full_name = os.path.join(dirname, file)
121                 if os.path.islink(full_name):
122                     fnames.remove(file)
123                 elif (os.path.isfile(full_name) and
124                       os.access(full_name, os.R_OK)):
125                     f = open(full_name, "r")
126                     cache[full_name.replace(os.path.join(self.dir, ''),
127                                             '')] = f.read().rstrip()
128                     f.close()
129         os.path.walk(self.dir, add_to_cache, self.cache)
130
131
132 def adjust_lim(goal, curr):
133     gh = goal[0]
134     gs = goal[1]
135     gm = goal[2]
136     soft = curr[0]
137     hard = curr[1]
138     if gm != VC_LIM_KEEP:
139         if gm > soft or gm == resource.RLIM_INFINITY:
140             soft = gm
141         if gm > hard or gm == resource.RLIM_INFINITY:
142             hard = gm
143     if gs != VC_LIM_KEEP:
144         if gs > soft or gs == resource.RLIM_INFINITY:
145             soft = gs
146     if gh != VC_LIM_KEEP:
147         if gh > hard or gh == resource.RLIM_INFINITY:
148             hard = gh
149     return (soft, hard)
150
151
152 class VServer:
153
154     def __init__(self, name, vm_id = None, vm_running = None, logfile=None):
155
156         self.name = name
157         self.dir = "%s/%s" % (vserverimpl.VSERVER_BASEDIR, name)
158         if not (os.path.isdir(self.dir) and
159                 os.access(self.dir, os.R_OK | os.W_OK | os.X_OK)):
160             raise NoSuchVServer, "no such vserver: " + name
161         self.config = VServerConfig(name, "/etc/vservers/%s" % name)
162         self.remove_caps = ~vserverimpl.CAP_SAFE;
163         if vm_id == None:
164             vm_id = int(self.config.get('context'))
165         self.ctx = vm_id
166         if vm_running == None:
167             vm_running = self.is_running()
168         self.vm_running = vm_running
169         self.logfile = logfile
170
171     # inspired from nodemanager's logger
172     def log_in_file (self, fd, msg):
173         if not msg: msg="\n"
174         if not msg.endswith('\n'): msg += '\n'
175         os.write(fd, '%s: %s' % (time.asctime(time.gmtime()), msg))
176
177     def log(self,msg):
178         if self.logfile:
179             try:
180                 fd = os.open(self.logfile,os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0600)
181                 self.log_in_file(fd,msg)
182                 os.close(fd)
183             except:
184                 print '%s: (%s failed to open) %s'%(time.asctime(time.gmtime()),self.logfile,msg)
185
186     def set_rlimit(self, type, hard, soft, min):
187         """Generic set resource limit function for vserver"""
188         global RLIMITS
189         update = False
190
191         if hard <> VC_LIM_KEEP:
192             self.config.update('rlimits/%s.hard' % type.lower(), hard)
193             update = True
194         if soft <> VC_LIM_KEEP:
195             self.config.update('rlimits/%s.soft' % type.lower(), soft)
196             update = True
197         if min <> VC_LIM_KEEP:
198             self.config.update('rlimits/%s.min' % type.lower(), min)
199             update = True
200
201         if self.is_running() and update:
202             resource_type = RLIMITS[type]
203             try:
204                 vserverimpl.setrlimit(self.ctx, resource_type, hard, soft, min)
205                 if hasattr(resource, 'RLIMIT_' + type):
206                     lim = resource.getrlimit(resource_type)
207                     lim = adjust_lim((hard, soft, min), lim)
208                     resource.setrlimit(resource_type, lim)
209             except OSError, e:
210                 self.log("Error: setrlimit(%d, %s, %d, %d, %d): %s"
211                          % (self.ctx, type.lower(), hard, soft, min, e))
212
213         return update
214
215     def get_prefix_from_capabilities(self, capabilities, prefix):
216         split_caps = capabilities.split(',')
217         return ",".join(["%s" % (c) for c in split_caps if c.startswith(prefix.upper()) or c.startswith(prefix.lower())])
218
219     def get_bcaps_from_capabilities(self, capabilities):
220         return self.get_prefix_from_capabilities(capabilities, "cap_")
221
222     def get_ccaps_from_capabilities(self, capabilities):
223         return self.get_prefix_from_capabilities(capabilities, "vxc_")
224
225     def set_capabilities_config(self, capabilities):
226         bcaps = self.get_bcaps_from_capabilities(capabilities)
227         ccaps = self.get_ccaps_from_capabilities(capabilities)
228         self.config.update('bcapabilities', bcaps)
229         self.config.update('ccapabilities', ccaps)
230         ret = vserverimpl.setbcaps(self.ctx, vserverimpl.text2bcaps(bcaps))
231         if ret > 0:
232             return ret
233         return vserverimpl.setccaps(self.ctx, vserverimpl.text2ccaps(ccaps))
234
235     def get_capabilities(self):
236         bcaps = vserverimpl.bcaps2text(vserverimpl.getbcaps(self.ctx))
237         ccaps = vserverimpl.ccaps2text(vserverimpl.getccaps(self.ctx))
238         if bcaps and ccaps:
239             ccaps = "," + ccaps
240         return (bcaps + ccaps)
241  
242     def get_capabilities_config(self):
243         bcaps = self.config.get('bcapabilities', '')
244         ccaps = self.config.get('ccapabilities', '')
245         if bcaps and ccaps:
246             ccaps = "," + ccaps
247         return (bcaps + ccaps)
248
249     def set_ipaddresses(self, addresses):
250         vserverimpl.netremove(self.ctx, "all")
251         for a in addresses.split(","):
252             vserverimpl.netadd(self.ctx, a)
253
254     def set_ipaddresses_config(self, addresses):
255         i = 0
256         for a in addresses.split(","):
257             self.config.update("interfaces/%d/ip" % i, a)
258             i += 1
259         while self.config.unset("interfaces/%d/ip" % i):
260             i += 1
261         self.set_ipaddresses(addresses)
262
263     def get_ipaddresses_config(self):
264         i = 0
265         ret = []
266         while True:
267             r = self.config.get("interfaces/%d/ip" % i, '')
268             if r == '':
269                 break
270             ret += [r]
271             i += 1
272         return ",".join(ret)
273
274     def get_ipaddresses(self):
275         # No clean way to do this right now.
276         return None
277
278     def __do_chroot(self):
279         os.chroot(self.dir)
280         os.chdir("/")
281
282     def chroot_call(self, fn, *args, **kwargs):
283
284         cwd_fd = os.open(".", os.O_RDONLY)
285         try:
286             root_fd = os.open("/", os.O_RDONLY)
287             try:
288                 self.__do_chroot()
289                 result = fn(*args, **kwargs)
290             finally:
291                 os.fchdir(root_fd)
292                 os.chroot(".")
293                 os.fchdir(cwd_fd)
294                 os.close(root_fd)
295         finally:
296             os.close(cwd_fd)
297         return result
298
299     def set_disklimit(self, block_limit):
300         # block_limit is in kB
301         if block_limit == 0:
302             try:
303                 vserverimpl.unsetdlimit(self.dir, self.ctx)
304             except OSError, e:
305                 self.log("Unexpected error with unsetdlimit for context %d: %r" % (self.ctx,e))
306             return
307
308         if self.vm_running:
309             block_usage = vserverimpl.DLIMIT_KEEP
310             inode_usage = vserverimpl.DLIMIT_KEEP
311         else:
312             # init_disk_info() must have been called to get usage values
313             block_usage = self.disk_blocks
314             inode_usage = self.disk_inodes
315
316         try:
317             vserverimpl.setdlimit(self.dir,
318                                   self.ctx,
319                                   block_usage,
320                                   block_limit,
321                                   inode_usage,
322                                   vserverimpl.DLIMIT_INF,  # inode limit
323                                   2)   # %age reserved for root
324         except OSError, e:
325             self.log("Unexpected error with setdlimit for context %d: %r" % (self.ctx, e))
326
327
328         self.config.update('dlimits/0/space_total', block_limit)
329
330     def is_running(self):
331         return vserverimpl.isrunning(self.ctx)
332     
333     def get_disklimit(self):
334
335         try:
336             (self.disk_blocks, block_limit, self.disk_inodes, inode_limit,
337              reserved) = vserverimpl.getdlimit(self.dir, self.ctx)
338         except OSError, ex:
339             if ex.errno != errno.ESRCH:
340                 raise
341             # get here if no vserver disk limit has been set for xid
342             block_limit = -1
343
344         return block_limit
345
346     def set_sched_config(self, cpu_min, cpu_share):
347
348         """ Write current CPU scheduler parameters to the vserver
349         configuration file. This method does not modify the kernel CPU
350         scheduling parameters for this context. """
351
352         self.config.update('sched/fill-rate', cpu_min)
353         self.config.update('sched/fill-rate2', cpu_share)
354         if cpu_share == 0:
355             self.config.unset('sched/idle-time')
356         
357         if self.is_running():
358             self.set_sched(cpu_min, cpu_share)
359
360     def set_sched(self, cpu_min, cpu_share):
361         """ Update kernel CPU scheduling parameters for this context. """
362         vserverimpl.setsched(self.ctx, cpu_min, cpu_share)
363
364     def get_sched(self):
365         # have no way of querying scheduler right now on a per vserver basis
366         return (-1, False)
367
368     def set_bwlimit(self, minrate = bwlimit.bwmin, maxrate = None,
369                     exempt_min = None, exempt_max = None,
370                     share = None, dev = "eth0"):
371
372         if minrate is None:
373             bwlimit.off(self.ctx, dev)
374         else:
375             bwlimit.on(self.ctx, dev, share,
376                        minrate, maxrate, exempt_min, exempt_max)
377
378     def get_bwlimit(self, dev = "eth0"):
379
380         result = bwlimit.get(self.ctx)
381         # result of bwlimit.get is (ctx, share, minrate, maxrate)
382         if result:
383             result = result[1:]
384         return result
385
386     def open(self, filename, mode = "r", bufsize = -1):
387
388         return self.chroot_call(open, filename, mode, bufsize)
389
390     def __do_chcontext(self, state_file):
391
392         if state_file:
393             print >>state_file, "%u" % self.ctx
394             state_file.close()
395
396         if vserverimpl.chcontext(self.ctx, vserverimpl.text2bcaps(self.get_capabilities_config())):
397             self.set_resources(True)
398             vserverimpl.setup_done(self.ctx)
399
400
401     def __prep(self, runlevel):
402
403         """ Perform all the crap that the vserver script does before
404         actually executing the startup scripts. """
405
406
407         # set the initial runlevel
408         vserverimpl.setrunlevel(self.dir + "/var/run/utmp", runlevel)
409
410         # mount /proc and /dev/pts
411         self.__do_mount("none", self.dir, "/proc", "proc")
412         # XXX - magic mount options
413         self.__do_mount("none", self.dir, "/dev/pts", "devpts", 0, "gid=5,mode=0620")
414
415
416     def __cleanvar(self):
417         """
418         Clean the /var/ directory so RH startup scripts can run
419         """ 
420
421         RUNDIR = "/var/run"
422         LOCKDIR = "/var/lock/subsys"
423
424         filter = ["utmp"]
425         garbage = []
426         for topdir in [RUNDIR, LOCKDIR]:
427             #os.walk() = (dirpath, dirnames, filenames)
428             for root, dirs, files in os.walk(topdir):
429                 for file in files:
430                     if not file in filter:
431                         garbage.append(root + "/" + file)
432
433         for f in garbage: os.unlink(f)
434         return garbage
435
436
437     def __do_mount(self, *mount_args):
438         try:
439             vserverimpl.mount(*mount_args)
440         except OSError, ex:
441             if ex.errno == errno.EBUSY:
442                 # assume already mounted
443                 return
444             raise ex
445
446
447     def enter(self):
448         self.config.cache_it()
449         self.__do_chroot()
450         self.__do_chcontext(None)
451
452     # 2010 June 21 - Thierry 
453     # the slice initscript now gets invoked through rc - see sliver_vs.py in nodemanager
454     # and, rc is triggered as part of vserver .. start 
455     # so we don't have to worry about initscripts at all anymore here
456     def start(self, runlevel = 3):
457         if os.fork() != 0:
458             # Parent should just return.
459             self.vm_running = True
460             return
461         else:
462             os.setsid()
463             # first child process: fork again
464             if os.fork() != 0:
465                 os._exit(0)     # Exit parent (the first child) of the second child.
466             # the grandson is the working one
467             os.chdir('/')
468             os.umask(0)
469             try:
470                 # start the vserver
471                 subprocess.call(["/usr/sbin/vserver",self.name,"start"])
472
473             # we get here due to an exception in the grandson process
474             except Exception, ex:
475                 self.log(traceback.format_exc())
476             os._exit(0)
477
478
479     def set_resources(self,setup=False):
480
481         """ Called when vserver context is entered for first time,
482         should be overridden by subclass. """
483
484         pass
485
486     def init_disk_info(self):
487         try:
488             dlimit = vserverimpl.getdlimit(self.dir, self.ctx)
489             self.disk_blocks = dlimit[0]
490             self.disk_inodes = dlimit[2]
491             return self.disk_blocks * 1024
492         except Exception, e:
493             pass
494         cmd = "/usr/sbin/vdu --script --space --inodes --blocksize 1024 --xid %d %s" % (self.ctx, self.dir)
495         p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
496                              stdout=subprocess.PIPE, stderr=subprocess.PIPE,
497                              close_fds=True)
498         p.stdin.close()
499         line = p.stdout.readline()
500         if not line:
501             sys.stderr.write(p.stderr.read())
502         p.stdout.close()
503         p.stderr.close()
504         ret = p.wait()
505
506         (space, inodes) = line.split()
507         self.disk_inodes = int(inodes)
508         self.disk_blocks = int(space)
509         #(self.disk_inodes, self.disk_blocks) = vduimpl.vdu(self.dir)
510
511         return self.disk_blocks * 1024
512
513     def stop(self, signal = signal.SIGKILL):
514         vserverimpl.killall(self.ctx, signal)
515         self.vm_running = False
516
517     def setname(self, slice_id):
518         '''Set vcVHI_CONTEXT field in kernel to slice_id'''
519         vserverimpl.setname(self.ctx, slice_id)
520
521     def getname(self):
522         '''Get vcVHI_CONTEXT field in kernel'''
523         return vserverimpl.getname(self.ctx)
524
525
526 def create(vm_name, static = False, ctor = VServer):
527
528     options = ['vuseradd']
529     if static:
530         options += ['--static']
531     ret = os.spawnvp(os.P_WAIT, 'vuseradd', options + [vm_name])
532     if not os.WIFEXITED(ret) or os.WEXITSTATUS(ret) != 0:
533         out = "system command ('%s') " % options
534         if os.WIFEXITED(ret):
535             out += "failed, rc = %d" % os.WEXITSTATUS(ret)
536         else:
537             out += "killed by signal %d" % os.WTERMSIG(ret)
538         raise SystemError, out
539     vm_id = pwd.getpwnam(vm_name)[2]
540
541     return ctor(vm_name, vm_id)
542
543
544 def close_nonstandard_fds():
545     """Close all open file descriptors other than 0, 1, and 2."""
546     _SC_OPEN_MAX = 4
547     for fd in range(3, os.sysconf(_SC_OPEN_MAX)):
548         try: os.close(fd)
549         except OSError: pass  # most likely an fd that isn't open
550