support for multiple slice reference images
[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             # however for cases like when vref is 'planetflow', if we do not understand
78             # any of the wishes we take vref as is
79             # this could be improved by having the vserver-reference init script be a bit smarter
80             # so we could take planetflow as the pldistro part here
81             as_is=None
82             # defaults
83             default=file("/etc/planetlab/defaultvref").read()
84             (pldistro,fcdistro,arch) = default.split("-")
85             # from the slice attribute: cut dashes and try to figure the meaning
86             slice_wishes = vref.split("-")
87             for wish in slice_wishes:
88                 if wish == "i386" or wish == "x86_64":
89                     arch=wish
90                 elif wish == "planetlab" or wish == "onelab" or wish == "vini":
91                     pldistro=wish
92                 elif wish == "f8" or wish == "centos5" :
93                     fcdistro=wish
94                 else:
95                     # if we find something like e.g. planetflow, use it as-is
96                     as_is=vref
97                     break
98             if as_is:
99                 refname=as_is
100             else:
101                 refname="-".join( (pldistro,fcdistro,arch) )
102             # check the templates exists -- there's probably a better way..
103             if not os.path.isdir ("/vservers/.vref/%s"%refname):
104                 log.verbose("%s (%s) : vref %s not found, using default %s"%(
105                         name,vref,refname,default))
106                 refname=default
107             except IOError:
108                 # have not found defaultvref
109                 logger.verbose("%s (%s): using fallback vrefname 'default'"%(name,vref))
110                 # for legacy nodes
111                 refname="default"
112             except:
113                 import traceback
114                 logger.log("%s (%s) : unexpected error follows - using 'default'"%(
115                         name,vref))
116                 logger.log(traceback.format_exc())
117             
118             logger.log_call('/usr/sbin/vuseradd', '-t', refname, name)
119         open('/vservers/%s/etc/slicename' % name, 'w').write(name)
120
121     @staticmethod
122     def destroy(name): logger.log_call('/usr/sbin/vuserdel', name)
123
124     def configure(self, rec):
125         new_rspec = rec['_rspec']
126         if new_rspec != self.rspec:
127             self.rspec = new_rspec
128             self.set_resources()
129
130         new_initscript = rec['initscript']
131         if new_initscript != self.initscript:
132             self.initscript = new_initscript
133             logger.log('%s: installing initscript' % self.name)
134             def install_initscript():
135                 flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
136                 fd = os.open('/etc/rc.vinit', flags, 0755)
137                 os.write(fd, new_initscript)
138                 os.close(fd)
139             try:
140                 self.chroot_call(install_initscript)
141                 self.initscriptchanged = True
142             except: logger.log_exc(self.name)
143
144         accounts.Account.configure(self, rec)  # install ssh keys
145
146     def start(self, delay=0):
147         if self.rspec['enabled'] > 0:
148             logger.log('%s: starting in %d seconds' % (self.name, delay))
149             time.sleep(delay)
150             child_pid = os.fork()
151             if child_pid == 0:
152                 # VServer.start calls fork() internally, so just close the nonstandard fds and fork once to avoid creating zombies
153                 tools.close_nonstandard_fds()
154                 vserver.VServer.start(self, True)
155                 os._exit(0)
156             else: os.waitpid(child_pid, 0)
157         else: logger.log('%s: not starting, is not enabled' % self.name)
158         self.initscriptchanged = False
159
160     def stop(self):
161         logger.log('%s: stopping' % self.name)
162         vserver.VServer.stop(self)
163
164     def set_resources(self):
165         disk_max = self.rspec['disk_max']
166         logger.log('%s: setting max disk usage to %d KiB' % (self.name, disk_max))
167         try:  # if the sliver is over quota, .set_disk_limit will throw an exception
168             if not self.disk_usage_initialized:
169                 self.vm_running = False
170                 logger.log('%s: computing disk usage: beginning' % self.name)
171                 Sliver_VS._init_disk_info_sem.acquire()
172                 try: self.init_disk_info()
173                 finally: Sliver_VS._init_disk_info_sem.release()
174                 logger.log('%s: computing disk usage: ended' % self.name)
175                 self.disk_usage_initialized = True
176             vserver.VServer.set_disklimit(self, max(disk_max, self.disk_blocks))
177         except:
178             logger.log('%s: failed to set max disk usage' % self.name)
179             logger.log_exc(self.name)
180
181         # get/set the min/soft/hard values for all of the vserver
182         # related RLIMITS.  Note that vserver currently only
183         # implements support for hard limits.
184         for limit in vserver.RLIMITS.keys():
185             type = limit.lower()
186             minimum  = self.rspec['%s_min'%type]
187             soft = self.rspec['%s_soft'%type]
188             hard = self.rspec['%s_hard'%type]
189             update = self.set_rlimit(limit, hard, soft, minimum)
190             if update:
191                 logger.log('%s: setting rlimit %s to (%d, %d, %d)'
192                            % (self.name, type, hard, soft, minimum))
193
194         self.set_capabilities_config(self.rspec['capabilities'])
195         if self.rspec['capabilities']:
196             logger.log('%s: setting capabilities to %s' % (self.name, self.rspec['capabilities']))
197
198         if False: # this code was commented out before
199             # N.B. net_*_rate are in kbps because of XML-RPC maxint
200             # limitations, convert to bps which is what bwlimit.py expects.
201             net_limits = (self.rspec['net_min_rate'] * 1000,
202                           self.rspec['net_max_rate'] * 1000,
203                           self.rspec['net_i2_min_rate'] * 1000,
204                           self.rspec['net_i2_max_rate'] * 1000,
205                           self.rspec['net_share'])
206             logger.log('%s: setting net limits to %s bps' % (self.name, net_limits[:-1]))
207             logger.log('%s: setting net share to %d' % (self.name, net_limits[-1]))
208             self.set_bwlimit(*net_limits)
209
210         cpu_pct = self.rspec['cpu_pct']
211         cpu_share = self.rspec['cpu_share']
212
213         if self.rspec['enabled'] > 0:
214             if cpu_pct > 0:
215                 logger.log('%s: setting cpu reservation to %d%%' % (self.name, cpu_pct))
216             else:
217                 cpu_pct = 0
218
219             if cpu_share > 0:
220                 logger.log('%s: setting cpu share to %d' % (self.name, cpu_share))
221             else:
222                 cpu_share = 0
223
224             self.set_sched_config(cpu_pct, cpu_share)
225             # if IP address isn't set (even to 0.0.0.0), sliver won't be able to use network
226             if self.rspec['ip_addresses'] != '0.0.0.0':
227                 logger.log('%s: setting IP address(es) to %s' % (self.name, self.rspec['ip_addresses']))
228             self.set_ipaddresses_config(self.rspec['ip_addresses'])
229
230             if False: # Does not work properly yet.
231                 if self.have_limits_changed():
232                     logger.log('%s: limits have changed --- restarting' % self.name)
233                     stopcount = 10
234                     while self.is_running() and stopcount > 0:
235                         self.stop()
236                         delay = 1
237                         time.sleep(delay)
238                         stopcount = stopcount - 1
239                     self.start()
240
241         else:  # tell vsh to disable remote login by setting CPULIMIT to 0
242             logger.log('%s: disabling remote login' % self.name)
243             self.set_sched_config(0, 0)
244             self.stop()