multiple slice falvours - second iteration
[nodemanager.git] / sliver_vs.py
1 """VServer slivers.
2
3 There are a couple of tricky things going on here.  First, the kernel
4 needs disk usage information in order to enforce the quota.  However,
5 determining disk usage redundantly strains the disks.  Thus, the
6 Sliver_VS.disk_usage_initialized flag is used to determine whether
7 this initialization has been made.
8
9 Second, it's not currently possible to set the scheduler parameters
10 for a sliver unless that sliver has a running process.  /bin/vsh helps
11 us out by reading the configuration file so that it can set the
12 appropriate limits after entering the sliver context.  Making the
13 syscall that actually sets the parameters gives a harmless error if no
14 process is running.  Thus we keep vm_running on when setting scheduler
15 parameters so that set_sched_params() always makes the syscall, and we
16 don't have to guess if there is a running process or not.
17 """
18
19 import errno
20 import os, os.path
21 import time
22 import vserver
23
24 import accounts
25 import logger
26 import tools
27
28 # special constant that tells vserver to keep its existing settings
29 KEEP_LIMIT = vserver.VC_LIM_KEEP
30
31 # populate the sliver/vserver specific default allocations table,
32 # which is used to look for slice attributes
33 DEFAULT_ALLOCATION = {}
34 for rlimit in vserver.RLIMITS.keys():
35     rlim = rlimit.lower()
36     DEFAULT_ALLOCATION["%s_min"%rlim]=KEEP_LIMIT
37     DEFAULT_ALLOCATION["%s_soft"%rlim]=KEEP_LIMIT
38     DEFAULT_ALLOCATION["%s_hard"%rlim]=KEEP_LIMIT
39
40 class Sliver_VS(accounts.Account, vserver.VServer):
41     """This class wraps vserver.VServer to make its interface closer to what we need."""
42
43     SHELL = '/bin/vsh'
44     TYPE = 'sliver.VServer'
45     _init_disk_info_sem = tools.NMLock("/var/run/nm-disk-info.lock")
46
47     def __init__(self, rec):
48         logger.verbose ('initing Sliver_VS with name=%s'%rec['name'])
49         try:
50             vserver.VServer.__init__(self, rec['name'],logfile='/var/log/nm')
51         except Exception, err:
52             if not isinstance(err, vserver.NoSuchVServer):
53                 # Probably a bad vserver or vserver configuration file
54                 logger.log_exc(self.name)
55                 logger.log('%s: recreating bad vserver' % rec['name'])
56                 self.destroy(rec['name'])
57             self.create(rec['name'], rec['vref'])
58             vserver.VServer.__init__(self, rec['name'],logfile='/var/log/nm')
59
60         self.keys = ''
61         self.rspec = {}
62         self.initscript = ''
63         self.disk_usage_initialized = False
64         self.initscriptchanged = False
65         self.configure(rec)
66
67     @staticmethod
68     def create(name, vref = None):
69         logger.verbose('Sliver_VS:create - name=%s'%name)
70         if vref is None:
71             vref='default'
72         try:
73             ### locating the right slicefamily
74             # this is a first draft, and more a proof of concept thing
75             # the idea is to parse vref for dash-separated wishes,
76             # and to project these against the defaults
77             # so e.g. if the default slice family (as found in /etc/planetlab/slicefamily)
78             # is planetlab-f8-i386, then here is what we get
79             # vref=x86_64             -> vuseradd -t planetlab-f8-x86_64 
80             # vref=centos5            -> vuseradd -t planetlab-centos5-i386 
81             # vref=centos5-onelab     -> vuseradd -t onelab-centos5-i386 
82             # vref=planetflow         -> vuseradd -t planetflow-f8-i386
83             # vref=x86_64-planetflow  -> vuseradd -t planetflow-f8-x86_64
84
85             # default
86             default=file("/etc/planetlab/slicefamily").read().strip()
87             (pldistro,fcdistro,arch) = default.split("-")
88             # from the slice attribute: cut dashes and try to figure the meaning
89             slice_wishes = vref.split("-")
90             for wish in slice_wishes:
91                 if wish == "i386" or wish == "x86_64":
92                     arch=wish
93                 elif wish == "f8" or wish == "centos5" :
94                     fcdistro=wish
95                 else:
96                     pldistro=wish
97
98             # rejoin the parts
99             refname="-".join( (pldistro,fcdistro,arch) )
100
101             # check the templates exists -- there's probably a better way..
102             if not os.path.isdir ("/vservers/.vref/%s"%refname):
103                 logger.verbose("%s (%s) : vref %s not found, using default %s"%(
104                         name,vref,refname,default))
105                 refname=default
106                 # could check again, but as we have /etc/slicefamily 
107                 # there's probably no /vservers/.vref/default
108
109         except IOError:
110             # have not found slicefamily
111             logger.verbose("%s (%s): legacy node - using fallback vrefname 'default'"%(name,vref))
112                 # for legacy nodes
113             refname="default"
114         except:
115             import traceback
116             logger.log("%s (%s) : unexpected error follows - using 'default'"%(
117                     name,vref))
118             logger.log(traceback.format_exc())
119             refname="default"
120             
121         logger.log_call('/usr/sbin/vuseradd', '-t', refname, name)
122         open('/vservers/%s/etc/slicename' % name, 'w').write(name)
123
124     @staticmethod
125     def destroy(name): logger.log_call('/usr/sbin/vuserdel', name)
126
127     def configure(self, rec):
128         new_rspec = rec['_rspec']
129         if new_rspec != self.rspec:
130             self.rspec = new_rspec
131             self.set_resources()
132
133         new_initscript = rec['initscript']
134         if new_initscript != self.initscript:
135             self.initscript = new_initscript
136             logger.log('%s: installing initscript' % self.name)
137             def install_initscript():
138                 flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
139                 fd = os.open('/etc/rc.vinit', flags, 0755)
140                 os.write(fd, new_initscript)
141                 os.close(fd)
142             try:
143                 self.chroot_call(install_initscript)
144                 self.initscriptchanged = True
145             except: logger.log_exc(self.name)
146
147         accounts.Account.configure(self, rec)  # install ssh keys
148
149     def start(self, delay=0):
150         if self.rspec['enabled'] > 0:
151             logger.log('%s: starting in %d seconds' % (self.name, delay))
152             time.sleep(delay)
153             child_pid = os.fork()
154             if child_pid == 0:
155                 # VServer.start calls fork() internally, so just close the nonstandard fds and fork once to avoid creating zombies
156                 tools.close_nonstandard_fds()
157                 vserver.VServer.start(self, True)
158                 os._exit(0)
159             else: os.waitpid(child_pid, 0)
160         else: logger.log('%s: not starting, is not enabled' % self.name)
161         self.initscriptchanged = False
162
163     def stop(self):
164         logger.log('%s: stopping' % self.name)
165         vserver.VServer.stop(self)
166
167     def set_resources(self):
168         disk_max = self.rspec['disk_max']
169         logger.log('%s: setting max disk usage to %d KiB' % (self.name, disk_max))
170         try:  # if the sliver is over quota, .set_disk_limit will throw an exception
171             if not self.disk_usage_initialized:
172                 self.vm_running = False
173                 logger.log('%s: computing disk usage: beginning' % self.name)
174                 Sliver_VS._init_disk_info_sem.acquire()
175                 try: self.init_disk_info()
176                 finally: Sliver_VS._init_disk_info_sem.release()
177                 logger.log('%s: computing disk usage: ended' % self.name)
178                 self.disk_usage_initialized = True
179             vserver.VServer.set_disklimit(self, max(disk_max, self.disk_blocks))
180         except:
181             logger.log('%s: failed to set max disk usage' % self.name)
182             logger.log_exc(self.name)
183
184         # get/set the min/soft/hard values for all of the vserver
185         # related RLIMITS.  Note that vserver currently only
186         # implements support for hard limits.
187         for limit in vserver.RLIMITS.keys():
188             type = limit.lower()
189             minimum  = self.rspec['%s_min'%type]
190             soft = self.rspec['%s_soft'%type]
191             hard = self.rspec['%s_hard'%type]
192             update = self.set_rlimit(limit, hard, soft, minimum)
193             if update:
194                 logger.log('%s: setting rlimit %s to (%d, %d, %d)'
195                            % (self.name, type, hard, soft, minimum))
196
197         self.set_capabilities_config(self.rspec['capabilities'])
198         if self.rspec['capabilities']:
199             logger.log('%s: setting capabilities to %s' % (self.name, self.rspec['capabilities']))
200
201         if False: # this code was commented out before
202             # N.B. net_*_rate are in kbps because of XML-RPC maxint
203             # limitations, convert to bps which is what bwlimit.py expects.
204             net_limits = (self.rspec['net_min_rate'] * 1000,
205                           self.rspec['net_max_rate'] * 1000,
206                           self.rspec['net_i2_min_rate'] * 1000,
207                           self.rspec['net_i2_max_rate'] * 1000,
208                           self.rspec['net_share'])
209             logger.log('%s: setting net limits to %s bps' % (self.name, net_limits[:-1]))
210             logger.log('%s: setting net share to %d' % (self.name, net_limits[-1]))
211             self.set_bwlimit(*net_limits)
212
213         cpu_pct = self.rspec['cpu_pct']
214         cpu_share = self.rspec['cpu_share']
215
216         if self.rspec['enabled'] > 0:
217             if cpu_pct > 0:
218                 logger.log('%s: setting cpu reservation to %d%%' % (self.name, cpu_pct))
219             else:
220                 cpu_pct = 0
221
222             if cpu_share > 0:
223                 logger.log('%s: setting cpu share to %d' % (self.name, cpu_share))
224             else:
225                 cpu_share = 0
226
227             self.set_sched_config(cpu_pct, cpu_share)
228             # if IP address isn't set (even to 0.0.0.0), sliver won't be able to use network
229             if self.rspec['ip_addresses'] != '0.0.0.0':
230                 logger.log('%s: setting IP address(es) to %s' % (self.name, self.rspec['ip_addresses']))
231             self.set_ipaddresses_config(self.rspec['ip_addresses'])
232
233             if False: # Does not work properly yet.
234                 if self.have_limits_changed():
235                     logger.log('%s: limits have changed --- restarting' % self.name)
236                     stopcount = 10
237                     while self.is_running() and stopcount > 0:
238                         self.stop()
239                         delay = 1
240                         time.sleep(delay)
241                         stopcount = stopcount - 1
242                     self.start()
243
244         else:  # tell vsh to disable remote login by setting CPULIMIT to 0
245             logger.log('%s: disabling remote login' % self.name)
246             self.set_sched_config(0, 0)
247             self.stop()