Merge branch 'ipv6'
[nodemanager.git] / sliver_vs.py
1 #
2
3 """VServer slivers.
4
5 There are a couple of tricky things going on here.  First, the kernel
6 needs disk usage information in order to enforce the quota.  However,
7 determining disk usage redundantly strains the disks.  Thus, the
8 Sliver_VS.disk_usage_initialized flag is used to determine whether
9 this initialization has been made.
10
11 Second, it's not currently possible to set the scheduler parameters
12 for a sliver unless that sliver has a running process.  /bin/vsh helps
13 us out by reading the configuration file so that it can set the
14 appropriate limits after entering the sliver context.  Making the
15 syscall that actually sets the parameters gives a harmless error if no
16 process is running.  Thus we keep vm_running on when setting scheduler
17 parameters so that set_sched_params() always makes the syscall, and we
18 don't have to guess if there is a running process or not.
19 """
20
21 import errno
22 import traceback
23 import os, os.path
24 import sys
25 import time
26 from threading import BoundedSemaphore
27 import subprocess
28
29 # the util-vserver-pl module
30 import vserver
31
32 import logger
33 import tools
34 from account import Account
35 from initscript import Initscript
36
37 # special constant that tells vserver to keep its existing settings
38 KEEP_LIMIT = vserver.VC_LIM_KEEP
39
40 # populate the sliver/vserver specific default allocations table,
41 # which is used to look for slice attributes
42 DEFAULT_ALLOCATION = {}
43 for rlimit in vserver.RLIMITS.keys():
44     rlim = rlimit.lower()
45     DEFAULT_ALLOCATION["%s_min"%rlim]=KEEP_LIMIT
46     DEFAULT_ALLOCATION["%s_soft"%rlim]=KEEP_LIMIT
47     DEFAULT_ALLOCATION["%s_hard"%rlim]=KEEP_LIMIT
48
49 class Sliver_VS(vserver.VServer, Account, Initscript):
50     """This class wraps vserver.VServer to make its interface closer to what we need."""
51
52     SHELL = '/bin/vsh'
53     TYPE = 'sliver.VServer'
54     _init_disk_info_sem = BoundedSemaphore()
55
56     def __init__(self, rec):
57         name=rec['name']
58         logger.verbose ('sliver_vs: %s init'%name)
59         try:
60             logger.log("sliver_vs: %s: first chance..."%name)
61             vserver.VServer.__init__(self, name,logfile='/var/log/nodemanager')
62             Account.__init__ (self, name)
63             Initscript.__init__ (self, name)
64         except Exception, err:
65             if not isinstance(err, vserver.NoSuchVServer):
66                 # Probably a bad vserver or vserver configuration file
67                 logger.log_exc("sliver_vs:__init__ (first chance) %s",name=name)
68                 logger.log('sliver_vs: %s: recreating bad vserver' % name)
69                 self.destroy(name)
70             self.create(name, rec)
71             vserver.VServer.__init__(self, name,logfile='/var/log/nodemanager')
72             Account.__init__ (self, name)
73             Initscript.__init__ (self, name)
74
75         self.rspec = {}
76         self.slice_id = rec['slice_id']
77         self.disk_usage_initialized = False
78         self.enabled = True
79         # xxx this almost certainly is wrong...
80         self.configure(rec)
81
82     @staticmethod
83     def create(name, rec = None):
84         logger.verbose('sliver_vs: %s: create'%name)
85         vref = rec['vref']
86         if vref is None:
87             # added by caglar
88             # band-aid for short period as old API doesn't have GetSliceFamily function
89             vref = "planetlab-f8-i386"
90             logger.log("sliver_vs: %s: ERROR - no vref attached, using hard-wired default %s"%(name,vref))
91
92         # used to look in /etc/planetlab/family,
93         # now relies on the 'GetSliceFamily' extra attribute in GetSlivers()
94         # which for legacy is still exposed here as the 'vref' key
95
96         # check the template exists -- there's probably a better way..
97         if not os.path.isdir ("/vservers/.vref/%s"%vref):
98             logger.log ("sliver_vs: %s: ERROR Could not create sliver - vreference image %s not found"%(name,vref))
99             return
100
101         # compute guest personality
102         try:
103             (x,y,arch)=vref.split('-')
104         # mh, this of course applies when 'vref' is e.g. 'netflow'
105         # and that's not quite right
106         except:
107             arch='i386'
108
109         def personality (arch): return "linux64" if arch.find("64") >=0 else "linux32"
110
111         command=[]
112         # be verbose
113         command += ['/bin/bash','-x',]
114         command += ['/usr/sbin/vuseradd', ]
115         if 'attributes' in rec and 'isolate_loopback' in rec['attributes'] and rec['attributes']['isolate_loopback'] == '1':
116             command += [ "-i",]
117         # the vsliver imge to use
118         command += [ '-t', vref, ]
119         # slice name
120         command += [ name, ]            
121         logger.log_call(command, timeout=15*60)
122         # export slicename to the slice in /etc/slicename
123         file('/vservers/%s/etc/slicename' % name, 'w').write(name)
124         file('/vservers/%s/etc/slicefamily' % name, 'w').write(vref)
125         # set personality: only if needed (if arch's differ)
126         if tools.root_context_arch() != arch:
127             file('/etc/vservers/%s/personality' % name, 'w').write(personality(arch)+"\n")
128             logger.log('sliver_vs: %s: set personality to %s'%(name,personality(arch)))
129
130     @staticmethod
131     def destroy(name):
132         # need to umount before we trash, otherwise we end up with sequels in 
133         # /vservers/slicename/ (namely in home/ )
134         # also because this is a static method we cannot check for 'omf_control'
135         # but it is no big deal as umount_ssh_dir checks before it umounts..
136         Account.umount_ssh_dir(name)
137         logger.log("sliver_vs: destroying %s"%name)
138         logger.log_call(['/bin/bash','-x','/usr/sbin/vuserdel', name, ])
139
140
141     def configure(self, rec):
142         # in case we update nodemanager..
143         self.install_and_enable_vinit()
144
145         new_rspec = rec['_rspec']
146         if new_rspec != self.rspec:
147             self.rspec = new_rspec
148             self.set_resources()
149
150         # do the configure part from Initscript
151         Initscript.configure(self,rec)
152
153         Account.configure(self, rec)  # install ssh keys
154
155     def start(self, delay=0):
156         if self.rspec['enabled'] <= 0:
157             logger.log('sliver_vs: not starting %s, is not enabled'%self.name)
158             return
159         logger.log('sliver_vs: %s: starting in %d seconds' % (self.name, delay))
160         time.sleep(delay)
161         # the generic /etc/init.d/vinit script is permanently refreshed, and enabled
162         self.install_and_enable_vinit()
163         # expose .ssh for omf_friendly slivers
164         if 'omf_control' in self.rspec['tags']:
165             Account.mount_ssh_dir(self.name)
166         # if a change has occured in the slice initscript, reflect this in /etc/init.d/vinit.slice
167         self.refresh_slice_vinit()
168         child_pid = os.fork()
169         if child_pid == 0:
170             # VServer.start calls fork() internally,
171             # so just close the nonstandard fds and fork once to avoid creating zombies
172             tools.close_nonstandard_fds()
173             vserver.VServer.start(self)
174             os._exit(0)
175         else:
176             os.waitpid(child_pid, 0)
177
178     def stop(self):
179         logger.log('sliver_vs: %s: stopping' % self.name)
180         vserver.VServer.stop(self)
181
182     def is_running(self):
183         return vserver.VServer.is_running(self)
184
185     # this one seems to belong in Initscript at first sight, 
186     # but actually depends on the underlying vm techno
187     # so let's keep it here
188     def rerun_slice_vinit(self):
189         command = "/usr/sbin/vserver %s exec /etc/rc.d/init.d/vinit restart" % (self.name)
190         logger.log("vsliver_vs: %s: Rerunning slice initscript: %s" % (self.name, command))
191         subprocess.call(command + "&", stdin=open('/dev/null', 'r'), stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT, shell=True)
192
193     def set_resources(self):
194         disk_max = self.rspec['disk_max']
195         logger.log('sliver_vs: %s: setting max disk usage to %d KiB' % (self.name, disk_max))
196         try:  # if the sliver is over quota, .set_disk_limit will throw an exception
197             if not self.disk_usage_initialized:
198                 self.vm_running = False
199                 Sliver_VS._init_disk_info_sem.acquire()
200                 logger.log('sliver_vs: %s: computing disk usage: beginning' % self.name)
201                 # init_disk_info is inherited from VServer
202                 try: self.init_disk_info()
203                 finally: Sliver_VS._init_disk_info_sem.release()
204                 logger.log('sliver_vs: %s: computing disk usage: ended' % self.name)
205                 self.disk_usage_initialized = True
206             vserver.VServer.set_disklimit(self, max(disk_max, self.disk_blocks))
207         except:
208             logger.log_exc('sliver_vs: failed to set max disk usage',name=self.name)
209
210         # get/set the min/soft/hard values for all of the vserver
211         # related RLIMITS.  Note that vserver currently only
212         # implements support for hard limits.
213         for limit in vserver.RLIMITS.keys():
214             type = limit.lower()
215             minimum  = self.rspec['%s_min'%type]
216             soft = self.rspec['%s_soft'%type]
217             hard = self.rspec['%s_hard'%type]
218             update = self.set_rlimit(limit, hard, soft, minimum)
219             if update:
220                 logger.log('sliver_vs: %s: setting rlimit %s to (%d, %d, %d)'
221                            % (self.name, type, hard, soft, minimum))
222
223         self.set_capabilities_config(self.rspec['capabilities'])
224         if self.rspec['capabilities']:
225             logger.log('sliver_vs: %s: setting capabilities to %s' % (self.name, self.rspec['capabilities']))
226
227         cpu_pct = self.rspec['cpu_pct']
228         cpu_share = self.rspec['cpu_share']
229
230         count = 1
231         for key in self.rspec.keys():
232             if key.find('sysctl.') == 0:
233                 sysctl=key.split('.')
234                 try:
235                     # /etc/vservers/<guest>/sysctl/<id>/
236                     dirname = "/etc/vservers/%s/sysctl/%s" % (self.name, count)
237                     try:
238                         os.makedirs(dirname, 0755)
239                     except:
240                         pass
241                     setting = open("%s/setting" % dirname, "w")
242                     setting.write("%s\n" % key.lstrip("sysctl."))
243                     setting.close()
244                     value = open("%s/value" % dirname, "w")
245                     value.write("%s\n" % self.rspec[key])
246                     value.close()
247                     count += 1
248
249                     logger.log("sliver_vs: %s: writing %s=%s"%(self.name,key,self.rspec[key]))
250                 except IOError, e:
251                     logger.log("sliver_vs: %s: could not set %s=%s"%(self.name,key,self.rspec[key]))
252                     logger.log("sliver_vs: %s: error = %s"%(self.name,e))
253
254
255         if self.rspec['enabled'] > 0:
256             if cpu_pct > 0:
257                 logger.log('sliver_vs: %s: setting cpu reservation to %d%%' % (self.name, cpu_pct))
258             else:
259                 cpu_pct = 0
260
261             if cpu_share > 0:
262                 logger.log('sliver_vs: %s: setting cpu share to %d' % (self.name, cpu_share))
263             else:
264                 cpu_share = 0
265
266             self.set_sched_config(cpu_pct, cpu_share)
267             # if IP address isn't set (even to 0.0.0.0), sliver won't be able to use network
268             if self.rspec['ip_addresses'] != '0.0.0.0':
269                 logger.log('sliver_vs: %s: setting IP address(es) to %s' % \
270                 (self.name, self.rspec['ip_addresses']))
271             add_loopback = True
272             if 'isolate_loopback' in self.rspec['tags']:
273                 add_loopback = self.rspec['tags']['isolate_loopback'] != "1"
274             self.set_ipaddresses_config(self.rspec['ip_addresses'], add_loopback)
275
276             #logger.log("sliver_vs: %s: Setting name to %s" % (self.name, self.slice_id))
277             #self.setname(self.slice_id)
278             #logger.log("sliver_vs: %s: Storing slice id of %s for PlanetFlow" % (self.name, self.slice_id))
279             try:
280                 vserver_config_path = '/etc/vservers/%s'%self.name
281                 if not os.path.exists (vserver_config_path):
282                     os.makedirs (vserver_config_path)
283                 file('%s/slice_id'%vserver_config_path, 'w').write("%d\n"%self.slice_id)
284                 logger.log("sliver_vs: Recorded slice id %d for slice %s"%(self.slice_id,self.name))
285             except IOError,e:
286                 logger.log("sliver_vs: Could not record slice_id for slice %s. Error: %s"%(self.name,str(e)))
287             except Exception,e:
288                 logger.log_exc("sliver_vs: Error recording slice id: %s"%str(e),name=self.name)
289
290
291             if self.enabled == False:
292                 self.enabled = True
293                 self.start()
294
295             if False: # Does not work properly yet.
296                 if self.have_limits_changed():
297                     logger.log('sliver_vs: %s: limits have changed --- restarting' % self.name)
298                     stopcount = 10
299                     while self.is_running() and stopcount > 0:
300                         self.stop()
301                         delay = 1
302                         time.sleep(delay)
303                         stopcount = stopcount - 1
304                     self.start()
305
306         else:  # tell vsh to disable remote login by setting CPULIMIT to 0
307             logger.log('sliver_vs: %s: disabling remote login' % self.name)
308             self.set_sched_config(0, 0)
309             self.enabled = False
310             self.stop()