Setting tag util-vserver-pl-0.3-35
[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             self.config.update("interfaces/%d/nodev" % i, "")
259             i += 1
260         while self.config.unset("interfaces/%d/ip" % i):
261             i += 1
262         self.set_ipaddresses(addresses)
263
264     def get_ipaddresses_config(self):
265         i = 0
266         ret = []
267         while True:
268             r = self.config.get("interfaces/%d/ip" % i, '')
269             if r == '':
270                 break
271             ret += [r]
272             i += 1
273         return ",".join(ret)
274
275     def get_ipaddresses(self):
276         # No clean way to do this right now.
277         return None
278
279     def __do_chroot(self):
280         os.chroot(self.dir)
281         os.chdir("/")
282
283     def chroot_call(self, fn, *args, **kwargs):
284
285         cwd_fd = os.open(".", os.O_RDONLY)
286         try:
287             root_fd = os.open("/", os.O_RDONLY)
288             try:
289                 self.__do_chroot()
290                 result = fn(*args, **kwargs)
291             finally:
292                 os.fchdir(root_fd)
293                 os.chroot(".")
294                 os.fchdir(cwd_fd)
295                 os.close(root_fd)
296         finally:
297             os.close(cwd_fd)
298         return result
299
300     def set_disklimit(self, block_limit):
301         # block_limit is in kB
302         if block_limit == 0:
303             try:
304                 vserverimpl.unsetdlimit(self.dir, self.ctx)
305             except OSError, e:
306                 self.log("Unexpected error with unsetdlimit for context %d: %r" % (self.ctx,e))
307             return
308
309         if self.vm_running:
310             block_usage = vserverimpl.DLIMIT_KEEP
311             inode_usage = vserverimpl.DLIMIT_KEEP
312         else:
313             # init_disk_info() must have been called to get usage values
314             block_usage = self.disk_blocks
315             inode_usage = self.disk_inodes
316
317         try:
318             vserverimpl.setdlimit(self.dir,
319                                   self.ctx,
320                                   block_usage,
321                                   block_limit,
322                                   inode_usage,
323                                   vserverimpl.DLIMIT_INF,  # inode limit
324                                   2)   # %age reserved for root
325         except OSError, e:
326             self.log("Unexpected error with setdlimit for context %d: %r" % (self.ctx, e))
327
328
329         self.config.update('dlimits/0/space_total', block_limit)
330
331     def is_running(self):
332         return vserverimpl.isrunning(self.ctx)
333     
334     def get_disklimit(self):
335
336         try:
337             (self.disk_blocks, block_limit, self.disk_inodes, inode_limit,
338              reserved) = vserverimpl.getdlimit(self.dir, self.ctx)
339         except OSError, ex:
340             if ex.errno != errno.ESRCH:
341                 raise
342             # get here if no vserver disk limit has been set for xid
343             block_limit = -1
344
345         return block_limit
346
347     def set_sched_config(self, cpu_min, cpu_share):
348
349         """ Write current CPU scheduler parameters to the vserver
350         configuration file. This method does not modify the kernel CPU
351         scheduling parameters for this context. """
352
353         self.config.update('sched/fill-rate', cpu_min)
354         self.config.update('sched/fill-rate2', cpu_share)
355         if cpu_share == 0:
356             self.config.unset('sched/idle-time')
357         
358         if self.is_running():
359             self.set_sched(cpu_min, cpu_share)
360
361     def set_sched(self, cpu_min, cpu_share):
362         """ Update kernel CPU scheduling parameters for this context. """
363         vserverimpl.setsched(self.ctx, cpu_min, cpu_share)
364
365     def get_sched(self):
366         # have no way of querying scheduler right now on a per vserver basis
367         return (-1, False)
368
369     def set_bwlimit(self, minrate = bwlimit.bwmin, maxrate = None,
370                     exempt_min = None, exempt_max = None,
371                     share = None, dev = "eth0"):
372
373         if minrate is None:
374             bwlimit.off(self.ctx, dev)
375         else:
376             bwlimit.on(self.ctx, dev, share,
377                        minrate, maxrate, exempt_min, exempt_max)
378
379     def get_bwlimit(self, dev = "eth0"):
380
381         result = bwlimit.get(self.ctx)
382         # result of bwlimit.get is (ctx, share, minrate, maxrate)
383         if result:
384             result = result[1:]
385         return result
386
387     def open(self, filename, mode = "r", bufsize = -1):
388
389         return self.chroot_call(open, filename, mode, bufsize)
390
391     def __do_chcontext(self, state_file):
392
393         if state_file:
394             print >>state_file, "%u" % self.ctx
395             state_file.close()
396
397         if vserverimpl.chcontext(self.ctx, vserverimpl.text2bcaps(self.get_capabilities_config())):
398             self.set_resources(True)
399             vserverimpl.setup_done(self.ctx)
400
401
402     def __prep(self, runlevel):
403
404         """ Perform all the crap that the vserver script does before
405         actually executing the startup scripts. """
406
407
408         # set the initial runlevel
409         vserverimpl.setrunlevel(self.dir + "/var/run/utmp", runlevel)
410
411         # mount /proc and /dev/pts
412         self.__do_mount("none", self.dir, "/proc", "proc")
413         # XXX - magic mount options
414         self.__do_mount("none", self.dir, "/dev/pts", "devpts", 0, "gid=5,mode=0620")
415
416
417     def __cleanvar(self):
418         """
419         Clean the /var/ directory so RH startup scripts can run
420         """ 
421
422         RUNDIR = "/var/run"
423         LOCKDIR = "/var/lock/subsys"
424
425         filter = ["utmp"]
426         garbage = []
427         for topdir in [RUNDIR, LOCKDIR]:
428             #os.walk() = (dirpath, dirnames, filenames)
429             for root, dirs, files in os.walk(topdir):
430                 for file in files:
431                     if not file in filter:
432                         garbage.append(root + "/" + file)
433
434         for f in garbage: os.unlink(f)
435         return garbage
436
437
438     def __do_mount(self, *mount_args):
439         try:
440             vserverimpl.mount(*mount_args)
441         except OSError, ex:
442             if ex.errno == errno.EBUSY:
443                 # assume already mounted
444                 return
445             raise ex
446
447
448     def enter(self):
449         self.config.cache_it()
450         self.__do_chroot()
451         self.__do_chcontext(None)
452
453     # 2010 June 21 - Thierry 
454     # the slice initscript now gets invoked through rc - see sliver_vs.py in nodemanager
455     # and, rc is triggered as part of vserver .. start 
456     # so we don't have to worry about initscripts at all anymore here
457     def start(self, runlevel = 3):
458         if os.fork() != 0:
459             # Parent should just return.
460             self.vm_running = True
461             return
462         else:
463             os.setsid()
464             # first child process: fork again
465             if os.fork() != 0:
466                 os._exit(0)     # Exit parent (the first child) of the second child.
467             # the grandson is the working one
468             os.chdir('/')
469             os.umask(0)
470             try:
471                 # start the vserver
472                 subprocess.call(["/usr/sbin/vserver",self.name,"start"])
473
474             # we get here due to an exception in the grandson process
475             except Exception, ex:
476                 self.log(traceback.format_exc())
477             os._exit(0)
478
479
480     def set_resources(self,setup=False):
481
482         """ Called when vserver context is entered for first time,
483         should be overridden by subclass. """
484
485         pass
486
487     def init_disk_info(self):
488         try:
489             dlimit = vserverimpl.getdlimit(self.dir, self.ctx)
490             self.disk_blocks = dlimit[0]
491             self.disk_inodes = dlimit[2]
492             return self.disk_blocks * 1024
493         except Exception, e:
494             pass
495         cmd = "/usr/sbin/vdu --script --space --inodes --blocksize 1024 --xid %d %s" % (self.ctx, self.dir)
496         p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
497                              stdout=subprocess.PIPE, stderr=subprocess.PIPE,
498                              close_fds=True)
499         p.stdin.close()
500         line = p.stdout.readline()
501         if not line:
502             sys.stderr.write(p.stderr.read())
503         p.stdout.close()
504         p.stderr.close()
505         ret = p.wait()
506
507         (space, inodes) = line.split()
508         self.disk_inodes = int(inodes)
509         self.disk_blocks = int(space)
510         #(self.disk_inodes, self.disk_blocks) = vduimpl.vdu(self.dir)
511
512         return self.disk_blocks * 1024
513
514     def stop(self, signal = signal.SIGKILL):
515         vserverimpl.killall(self.ctx, signal)
516         self.vm_running = False
517
518     def setname(self, slice_id):
519         '''Set vcVHI_CONTEXT field in kernel to slice_id'''
520         vserverimpl.setname(self.ctx, slice_id)
521
522     def getname(self):
523         '''Get vcVHI_CONTEXT field in kernel'''
524         return vserverimpl.getname(self.ctx)
525
526
527 def create(vm_name, static = False, ctor = VServer):
528
529     options = ['vuseradd']
530     if static:
531         options += ['--static']
532     ret = os.spawnvp(os.P_WAIT, 'vuseradd', options + [vm_name])
533     if not os.WIFEXITED(ret) or os.WEXITSTATUS(ret) != 0:
534         out = "system command ('%s') " % options
535         if os.WIFEXITED(ret):
536             out += "failed, rc = %d" % os.WEXITSTATUS(ret)
537         else:
538             out += "killed by signal %d" % os.WTERMSIG(ret)
539         raise SystemError, out
540     vm_id = pwd.getpwnam(vm_name)[2]
541
542     return ctor(vm_name, vm_id)
543
544
545 def close_nonstandard_fds():
546     """Close all open file descriptors other than 0, 1, and 2."""
547     _SC_OPEN_MAX = 4
548     for fd in range(3, os.sysconf(_SC_OPEN_MAX)):
549         try: os.close(fd)
550         except OSError: pass  # most likely an fd that isn't open
551