Setting tag nodemanager-1.8-32
[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 traceback
21 import os, os.path
22 import time
23
24 import vserver
25
26 import accounts
27 import logger
28 import tools
29 from threading import BoundedSemaphore
30
31 globalsem = BoundedSemaphore()
32
33 # special constant that tells vserver to keep its existing settings
34 KEEP_LIMIT = vserver.VC_LIM_KEEP
35
36 # populate the sliver/vserver specific default allocations table,
37 # which is used to look for slice attributes
38 DEFAULT_ALLOCATION = {}
39 for rlimit in vserver.RLIMITS.keys():
40     rlim = rlimit.lower()
41     DEFAULT_ALLOCATION["%s_min"%rlim]=KEEP_LIMIT
42     DEFAULT_ALLOCATION["%s_soft"%rlim]=KEEP_LIMIT
43     DEFAULT_ALLOCATION["%s_hard"%rlim]=KEEP_LIMIT
44
45 class Sliver_VS(accounts.Account, vserver.VServer):
46     """This class wraps vserver.VServer to make its interface closer to what we need."""
47
48     SHELL = '/bin/vsh'
49     TYPE = 'sliver.VServer'
50     _init_disk_info_sem = globalsem
51
52     def __init__(self, rec):
53         logger.verbose ('initing Sliver_VS with name=%s'%rec['name'])
54         try:
55             vserver.VServer.__init__(self, rec['name'],logfile='/var/log/nm')
56         except Exception, err:
57             if not isinstance(err, vserver.NoSuchVServer):
58                 # Probably a bad vserver or vserver configuration file
59                 logger.log_exc(self.name)
60                 logger.log('%s: recreating bad vserver' % rec['name'])
61                 self.destroy(rec['name'])
62             self.create(rec['name'], rec['vref'])
63             vserver.VServer.__init__(self, rec['name'],logfile='/var/log/nm')
64
65         self.keys = ''
66         self.rspec = {}
67         self.initscript = ''
68         self.slice_id = rec['slice_id']
69         self.disk_usage_initialized = False
70         self.initscriptchanged = False
71         self.enabled = True
72         self.configure(rec)
73
74     @staticmethod
75     def create(name, vref = None):
76         logger.verbose('Sliver_VS:create - name=%s'%name)
77         if vref is None:
78             vref='default'
79         try:
80             ### locating the right slicefamily
81             # this is a first draft, and more a proof of concept thing
82             # the idea is to parse vref for dash-separated wishes,
83             # and to project these against the defaults
84             # so e.g. if the default slice family (as found in /etc/planetlab/slicefamily)
85             # is planetlab-f8-i386, then here is what we get
86             # vref=x86_64             -> vuseradd -t planetlab-f8-x86_64 
87             # vref=centos5            -> vuseradd -t planetlab-centos5-i386 
88             # vref=centos5-onelab     -> vuseradd -t onelab-centos5-i386 
89             # vref=planetflow         -> vuseradd -t planetflow-f8-i386
90             # vref=x86_64-planetflow  -> vuseradd -t planetflow-f8-x86_64
91
92             # default
93             default=file("/etc/planetlab/slicefamily").read().strip()
94             (pldistro,fcdistro,arch) = default.split("-")
95
96             known_archs = [ 'i386', 'x86_64' ]
97             known_fcdistros = [ 'centos5', 'f8', 'f9', 'f10', 'f11', 'f12' ]
98             # from the slice attribute: cut dashes and try to figure the meaning
99             slice_wishes = vref.split("-")
100             for wish in slice_wishes:
101                 if wish in known_archs:
102                     arch=wish
103                 elif wish in known_fcdistros:
104                     fcdistro=wish
105                 else:
106                     pldistro=wish
107
108             # rejoin the parts
109             refname="-".join( (pldistro,fcdistro,arch) )
110
111             # check the template exists -- there's probably a better way..
112             if not os.path.isdir ("/vservers/.vref/%s"%refname):
113                 logger.log("%s (%s) : vref %s not found, using default %s"%(
114                         name,vref,refname,default))
115                 refname=default
116                 # reset so arch is right
117                 (pldistro,fcdistro,arch) = default.split("-")
118                 # could check again, but as we have /etc/slicefamily 
119                 # there's probably no /vservers/.vref/default
120
121         except IOError:
122             # have not found slicefamily
123             logger.log("%s (%s): legacy node - using fallback vrefname 'default'"%(name,vref))
124             # for legacy nodes
125             refname="default"
126             arch="i386"
127         except:
128             logger.log("%s (%s) : unexpected error follows - using 'default'"%(name,vref))
129             logger.log(traceback.format_exc())
130             refname="default"
131             arch="i386"
132             
133         def personality (arch):
134             personality="linux32"
135             if arch.find("64")>=0:
136                 personality="linux64"
137             return personality
138
139         logger.log_call('/usr/sbin/vuseradd', '-t', refname, name)
140         # export slicename to the slice in /etc/slicename
141         file('/vservers/%s/etc/slicename' % name, 'w').write(name)
142         # set personality: only if needed (if arch's differ)
143         if tools.root_context_arch() != arch:
144             file('/etc/vservers/%s/personality' % name, 'w').write(personality(arch))
145             logger.log('%s: set personality to %s'%(name,personality(arch)))
146
147     @staticmethod
148     def destroy(name): logger.log_call('/usr/sbin/vuserdel', name)
149
150     def configure(self, rec):
151         new_rspec = rec['_rspec']
152         if new_rspec != self.rspec:
153             self.rspec = new_rspec
154             self.set_resources()
155
156         new_initscript = rec['initscript']
157         if new_initscript != self.initscript:
158             self.initscript = new_initscript
159             # not used anymore, we always check against the installed script
160             #self.initscriptchanged = True
161             self.refresh_slice_vinit()
162
163         accounts.Account.configure(self, rec)  # install ssh keys
164
165     # unconditionnally install and enable the generic vinit script
166     # mimicking chkconfig for enabling the generic vinit script
167     # this is hardwired for runlevel 3
168     def install_and_enable_vinit (self):
169         vinit_source="/usr/share/NodeManager/sliver-initscripts/vinit"
170         vinit_script="/vservers/%s/etc/rc.d/init.d/vinit"%self.name
171         rc3_link="/vservers/%s/etc/rc.d/rc3.d/S99vinit"%self.name
172         rc3_target="../init.d/vinit"
173         # install in sliver
174         body=file(vinit_source).read()
175         if tools.replace_file_with_string(vinit_script,body,chmod=0755):
176             logger.log("vsliver_vs: %s: installed generic vinit rc script"%self.name)
177         # create symlink for runlevel 3
178         if not os.path.islink(rc3_link):
179             try:
180                 logger.log("vsliver_vs: %s: creating runlevel3 symlink %s"%(self.name,rc3_link))
181                 os.symlink(rc3_target,rc3_link)
182             except:
183                 logger.log_exc("vsliver_vs: %s: failed to create runlevel3 symlink %s"%rc3_link)
184
185     # this one checks for the existence of the slice initscript
186     # install or remove the slice inistscript, as instructed by the initscript tag
187     def refresh_slice_vinit(self):
188         body=self.initscript
189         sliver_initscript="/vservers/%s/etc/rc.d/init.d/vinit.slice"%self.name
190         if tools.replace_file_with_string(sliver_initscript,body,remove_if_empty=True,chmod=0755):
191             if body:
192                 logger.log("vsliver_vs: %s: Installed new initscript in %s"%(self.name,sliver_initscript))
193             else:
194                 logger.log("vsliver_vs: %s: Removed obsolete initscript %s"%(self.name,sliver_initscript))
195
196     def start(self, delay=0):
197         if self.rspec['enabled'] <= 0:
198             logger.log('sliver_vs: not starting %s, is not enabled'%self.name)
199         else:
200             logger.log('sliver_vs: %s: starting in %d seconds' % (self.name, delay))
201             time.sleep(delay)
202             # the generic /etc/init.d/vinit script is permanently refreshed, and enabled
203             self.install_and_enable_vinit()
204             # if a change has occured in the slice initscript, reflect this in /etc/init.d/vinit.slice
205             self.refresh_slice_vinit()
206             child_pid = os.fork()
207             if child_pid == 0:
208                 # VServer.start calls fork() internally,
209                 # so just close the nonstandard fds and fork once to avoid creating zombies
210                 tools.close_nonstandard_fds()
211                 vserver.VServer.start(self)
212                 os._exit(0)
213             else:
214                 os.waitpid(child_pid, 0)
215
216     def stop(self):
217         logger.log('%s: stopping' % self.name)
218         vserver.VServer.stop(self)
219
220     def is_running(self): 
221         return vserver.VServer.is_running(self)
222
223     def set_resources(self,setup=False):
224         disk_max = self.rspec['disk_max']
225         logger.log('%s: setting max disk usage to %d KiB' % (self.name, disk_max))
226         try:  # if the sliver is over quota, .set_disk_limit will throw an exception
227             if not self.disk_usage_initialized:
228                 self.vm_running = False
229                 Sliver_VS._init_disk_info_sem.acquire()
230                 logger.log('%s: computing disk usage: beginning' % self.name)
231                 try: self.init_disk_info()
232                 finally: Sliver_VS._init_disk_info_sem.release()
233                 logger.log('%s: computing disk usage: ended' % self.name)
234                 self.disk_usage_initialized = True
235             vserver.VServer.set_disklimit(self, max(disk_max, self.disk_blocks))
236         except:
237             logger.log('%s: failed to set max disk usage' % self.name)
238             logger.log_exc(self.name)
239
240         # get/set the min/soft/hard values for all of the vserver
241         # related RLIMITS.  Note that vserver currently only
242         # implements support for hard limits.
243         for limit in vserver.RLIMITS.keys():
244             type = limit.lower()
245             minimum  = self.rspec['%s_min'%type]
246             soft = self.rspec['%s_soft'%type]
247             hard = self.rspec['%s_hard'%type]
248             update = self.set_rlimit(limit, hard, soft, minimum)
249             if update:
250                 logger.log('%s: setting rlimit %s to (%d, %d, %d)'
251                            % (self.name, type, hard, soft, minimum))
252
253         self.set_capabilities_config(self.rspec['capabilities'])
254         if self.rspec['capabilities']:
255             logger.log('%s: setting capabilities to %s' % (self.name, self.rspec['capabilities']))
256
257         cpu_pct = self.rspec['cpu_pct']
258         cpu_share = self.rspec['cpu_share']
259
260         if setup:
261             for key in self.rspec.keys():
262                 if key.find('sysctl.') == 0:
263                     sysctl=key.split('.')
264                     try:
265                         path="/proc/sys/%s" % ("/".join(sysctl[1:]))
266                         logger.log("%s: opening %s"%(self.name,path))
267                         flags = os.O_WRONLY
268                         fd = os.open(path, flags)
269                         logger.log("%s: writing %s=%s"%(self.name,key,self.rspec[key]))
270                         os.write(fd,self.rspec[key])
271                         os.close(fd)
272                     except IOError, e:
273                         logger.log("%s: could not set %s=%s"%(self.name,key,self.rspec[key]))
274                         logger.log("%s: error = %s"%(self.name,e))
275
276
277         if self.rspec['enabled'] > 0:
278             if cpu_pct > 0:
279                 logger.log('%s: setting cpu reservation to %d%%' % (self.name, cpu_pct))
280             else:
281                 cpu_pct = 0
282
283             if cpu_share > 0:
284                 logger.log('%s: setting cpu share to %d' % (self.name, cpu_share))
285             else:
286                 cpu_share = 0
287
288             self.set_sched_config(cpu_pct, cpu_share)
289             # if IP address isn't set (even to 0.0.0.0), sliver won't be able to use network
290             if self.rspec['ip_addresses'] != '0.0.0.0':
291                 logger.log('%s: setting IP address(es) to %s' % \
292                 (self.name, self.rspec['ip_addresses']))
293             self.set_ipaddresses_config(self.rspec['ip_addresses'])
294
295             try:
296                 vserver_config_path = '/etc/vservers/%s'%self.name
297                 if not os.path.exists (vserver_config_path):
298                     os.makedirs (vserver_config_path)
299                 file('%s/slice_id'%vserver_config_path, 'w').write("%d"%self.slice_id)
300                 logger.log("sliver_vs: Recorded slice id %d for slice %s"%(self.slice_id,self.name))
301             except IOError,e:
302                 logger.log("sliver_vs: Could not record slice_id for slice %s. Error: %s"%(self.name,str(e)))
303             except Exception,e:
304                 logger.log_exc("sliver_vs: Error recording slice id: %s"%str(e),name=self.name)
305
306             if self.enabled == False:
307                 self.enabled = True
308                 self.start()
309  
310             if False: # Does not work properly yet.
311                 if self.have_limits_changed():
312                     logger.log('%s: limits have changed --- restarting' % self.name)
313                     stopcount = 10
314                     while self.is_running() and stopcount > 0:
315                         self.stop()
316                         delay = 1
317                         time.sleep(delay)
318                         stopcount = stopcount - 1
319                     self.start()
320
321         else:  # tell vsh to disable remote login by setting CPULIMIT to 0
322             logger.log('%s: disabling remote login' % self.name)
323             self.set_sched_config(0, 0)
324             self.enabled = False
325             self.stop()