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