Store the slice id of the current vserver on disk, rather than sticking it in the...
[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             self.initscriptchanged = True
160
161         accounts.Account.configure(self, rec)  # install ssh keys
162
163     def start(self, delay=0):
164         if self.rspec['enabled'] > 0:
165             logger.log('%s: starting in %d seconds' % (self.name, delay))
166             time.sleep(delay)
167             # VServer.start calls fork() internally, 
168             # so just close the nonstandard fds and fork once to avoid creating zombies
169             child_pid = os.fork()
170             if child_pid == 0:
171                 if self.initscriptchanged:
172                     logger.log('%s: installing initscript' % self.name)
173                     def install_initscript():
174                         flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
175                         fd = os.open('/etc/rc.vinit', flags, 0755)
176                         os.write(fd, self.initscript)
177                         os.close(fd)
178                     try:
179                         self.chroot_call(install_initscript)
180                     except: logger.log_exc(self.name)
181                 tools.close_nonstandard_fds()
182                 vserver.VServer.start(self)
183                 os._exit(0)
184             else: 
185                 os.waitpid(child_pid, 0)
186                 self.initscriptchanged = False
187         else: logger.log('%s: not starting, is not enabled' % self.name)
188
189     def stop(self):
190         logger.log('%s: stopping' % self.name)
191         vserver.VServer.stop(self)
192
193     def is_running(self): 
194         return vserver.VServer.is_running(self)
195
196     def set_resources(self,setup=False):
197         disk_max = self.rspec['disk_max']
198         logger.log('%s: setting max disk usage to %d KiB' % (self.name, disk_max))
199         try:  # if the sliver is over quota, .set_disk_limit will throw an exception
200             if not self.disk_usage_initialized:
201                 self.vm_running = False
202                 Sliver_VS._init_disk_info_sem.acquire()
203                 logger.log('%s: computing disk usage: beginning' % self.name)
204                 try: self.init_disk_info()
205                 finally: Sliver_VS._init_disk_info_sem.release()
206                 logger.log('%s: computing disk usage: ended' % self.name)
207                 self.disk_usage_initialized = True
208             vserver.VServer.set_disklimit(self, max(disk_max, self.disk_blocks))
209         except:
210             logger.log('%s: failed to set max disk usage' % self.name)
211             logger.log_exc(self.name)
212
213         # get/set the min/soft/hard values for all of the vserver
214         # related RLIMITS.  Note that vserver currently only
215         # implements support for hard limits.
216         for limit in vserver.RLIMITS.keys():
217             type = limit.lower()
218             minimum  = self.rspec['%s_min'%type]
219             soft = self.rspec['%s_soft'%type]
220             hard = self.rspec['%s_hard'%type]
221             update = self.set_rlimit(limit, hard, soft, minimum)
222             if update:
223                 logger.log('%s: setting rlimit %s to (%d, %d, %d)'
224                            % (self.name, type, hard, soft, minimum))
225
226         self.set_capabilities_config(self.rspec['capabilities'])
227         if self.rspec['capabilities']:
228             logger.log('%s: setting capabilities to %s' % (self.name, self.rspec['capabilities']))
229
230         cpu_pct = self.rspec['cpu_pct']
231         cpu_share = self.rspec['cpu_share']
232
233         if setup:
234             for key in self.rspec.keys():
235                 if key.find('sysctl.') == 0:
236                     sysctl=key.split('.')
237                     try:
238                         path="/proc/sys/%s" % ("/".join(sysctl[1:]))
239                         logger.log("%s: opening %s"%(self.name,path))
240                         flags = os.O_WRONLY
241                         fd = os.open(path, flags)
242                         logger.log("%s: writing %s=%s"%(self.name,key,self.rspec[key]))
243                         os.write(fd,self.rspec[key])
244                         os.close(fd)
245                     except IOError, e:
246                         logger.log("%s: could not set %s=%s"%(self.name,key,self.rspec[key]))
247                         logger.log("%s: error = %s"%(self.name,e))
248
249
250         if self.rspec['enabled'] > 0:
251             if cpu_pct > 0:
252                 logger.log('%s: setting cpu reservation to %d%%' % (self.name, cpu_pct))
253             else:
254                 cpu_pct = 0
255
256             if cpu_share > 0:
257                 logger.log('%s: setting cpu share to %d' % (self.name, cpu_share))
258             else:
259                 cpu_share = 0
260
261             self.set_sched_config(cpu_pct, cpu_share)
262             # if IP address isn't set (even to 0.0.0.0), sliver won't be able to use network
263             if self.rspec['ip_addresses'] != '0.0.0.0':
264                 logger.log('%s: setting IP address(es) to %s' % \
265                 (self.name, self.rspec['ip_addresses']))
266             self.set_ipaddresses_config(self.rspec['ip_addresses'])
267
268             if self.is_running():
269                 logger.log("%s: Storing slice id for PlanetFlow" % (self.name, self.slice_id),2)
270                 #self.setname(self.slice_id)
271                 file('/etc/vservers/%s/slice_id' % name, 'w').write(self.slice_id)
272
273             if self.enabled == False:
274                 self.enabled = True
275                 self.start()
276  
277             if False: # Does not work properly yet.
278                 if self.have_limits_changed():
279                     logger.log('%s: limits have changed --- restarting' % self.name)
280                     stopcount = 10
281                     while self.is_running() and stopcount > 0:
282                         self.stop()
283                         delay = 1
284                         time.sleep(delay)
285                         stopcount = stopcount - 1
286                     self.start()
287
288         else:  # tell vsh to disable remote login by setting CPULIMIT to 0
289             logger.log('%s: disabling remote login' % self.name)
290             self.set_sched_config(0, 0)
291             self.enabled = False
292             self.stop()