Should be able to explicitly specify as well.
[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 from threading import BoundedSemaphore
28
29 globalsem = BoundedSemaphore()
30
31 # special constant that tells vserver to keep its existing settings
32 KEEP_LIMIT = vserver.VC_LIM_KEEP
33
34 # populate the sliver/vserver specific default allocations table,
35 # which is used to look for slice attributes
36 DEFAULT_ALLOCATION = {}
37 for rlimit in vserver.RLIMITS.keys():
38     rlim = rlimit.lower()
39     DEFAULT_ALLOCATION["%s_min"%rlim]=KEEP_LIMIT
40     DEFAULT_ALLOCATION["%s_soft"%rlim]=KEEP_LIMIT
41     DEFAULT_ALLOCATION["%s_hard"%rlim]=KEEP_LIMIT
42
43 class Sliver_VS(accounts.Account, vserver.VServer):
44     """This class wraps vserver.VServer to make its interface closer to what we need."""
45
46     SHELL = '/bin/vsh'
47     TYPE = 'sliver.VServer'
48     _init_disk_info_sem = globalsem
49
50     def __init__(self, rec):
51         logger.verbose ('initing Sliver_VS with name=%s'%rec['name'])
52         try:
53             vserver.VServer.__init__(self, rec['name'],logfile='/var/log/nm')
54         except Exception, err:
55             if not isinstance(err, vserver.NoSuchVServer):
56                 # Probably a bad vserver or vserver configuration file
57                 logger.log_exc(self.name)
58                 logger.log('%s: recreating bad vserver' % rec['name'])
59                 self.destroy(rec['name'])
60             self.create(rec['name'], rec['vref'])
61             vserver.VServer.__init__(self, rec['name'],logfile='/var/log/nm')
62
63         self.keys = ''
64         self.rspec = {}
65         self.initscript = ''
66         self.slice_id = rec['slice_id']
67         self.disk_usage_initialized = False
68         self.initscriptchanged = False
69         self.configure(rec)
70
71     @staticmethod
72     def create(name, vref = None):
73         logger.verbose('Sliver_VS:create - name=%s'%name)
74         if vref is None:
75             vref='default'
76         try:
77             ### locating the right slicefamily
78             # this is a first draft, and more a proof of concept thing
79             # the idea is to parse vref for dash-separated wishes,
80             # and to project these against the defaults
81             # so e.g. if the default slice family (as found in /etc/planetlab/slicefamily)
82             # is planetlab-f8-i386, then here is what we get
83             # vref=x86_64             -> vuseradd -t planetlab-f8-x86_64 
84             # vref=centos5            -> vuseradd -t planetlab-centos5-i386 
85             # vref=centos5-onelab     -> vuseradd -t onelab-centos5-i386 
86             # vref=planetflow         -> vuseradd -t planetflow-f8-i386
87             # vref=x86_64-planetflow  -> vuseradd -t planetflow-f8-x86_64
88
89             # default
90             default=file("/etc/planetlab/slicefamily").read().strip()
91             (pldistro,fcdistro,arch) = default.split("-")
92
93             known_archs = [ 'i386', 'x86_64' ]
94             known_fcdistros = [ 'f8', 'f9', 'centos5' ]
95             # from the slice attribute: cut dashes and try to figure the meaning
96             slice_wishes = vref.split("-")
97             for wish in slice_wishes:
98                 if wish in known_archs:
99                     arch=wish
100                 elif wish in known_fcdistros:
101                     fcdistro=wish
102                 else:
103                     pldistro=wish
104
105             # rejoin the parts
106             refname="-".join( (pldistro,fcdistro,arch) )
107
108             # check the templates exists -- there's probably a better way..
109             if not (os.path.isdir ("/vservers/.vref/%s"% refname) or 
110             os.path.isdir ("/vservers/.vref/%s"% vref)):
111                 logger.verbose("%s (%s) : vref %s not found, using default %s"%(
112                         name,vref,refname,default))
113                 refname=default
114                 # could check again, but as we have /etc/slicefamily 
115                 # there's probably no /vservers/.vref/default
116
117         except IOError:
118             # have not found slicefamily
119             logger.verbose("%s (%s): legacy node - using fallback vrefname 'default'"%(name,vref))
120                 # for legacy nodes
121             refname="default"
122         except:
123             import traceback
124             logger.log("%s (%s) : unexpected error follows - using 'default'"%(
125                     name,vref))
126             logger.log(traceback.format_exc())
127             refname="default"
128             
129         logger.log_call('/usr/sbin/vuseradd', '-t', refname, name)
130         open('/vservers/%s/etc/slicename' % name, 'w').write(name)
131
132     @staticmethod
133     def destroy(name): logger.log_call('/usr/sbin/vuserdel', name)
134
135     def configure(self, rec):
136         new_rspec = rec['_rspec']
137         if new_rspec != self.rspec:
138             self.rspec = new_rspec
139             self.set_resources()
140
141         new_initscript = rec['initscript']
142         if new_initscript != self.initscript:
143             self.initscript = new_initscript
144             logger.log('%s: installing initscript' % self.name)
145             def install_initscript():
146                 flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
147                 fd = os.open('/etc/rc.vinit', flags, 0755)
148                 os.write(fd, new_initscript)
149                 os.close(fd)
150             try:
151                 self.chroot_call(install_initscript)
152                 self.initscriptchanged = True
153             except: logger.log_exc(self.name)
154
155         accounts.Account.configure(self, rec)  # install ssh keys
156
157     def start(self, delay=0):
158         if self.rspec['enabled'] > 0:
159             logger.log('%s: starting in %d seconds' % (self.name, delay))
160             time.sleep(delay)
161             child_pid = os.fork()
162             if child_pid == 0:
163                 # VServer.start calls fork() internally, 
164                 # so just close the nonstandard fds and fork once to avoid creating zombies
165                 tools.close_nonstandard_fds()
166                 vserver.VServer.start(self)
167                 os._exit(0)
168             else: os.waitpid(child_pid, 0)
169         else: logger.log('%s: not starting, is not enabled' % self.name)
170         self.initscriptchanged = False
171
172     def stop(self):
173         logger.log('%s: stopping' % self.name)
174         vserver.VServer.stop(self)
175
176     def set_resources(self):
177         disk_max = self.rspec['disk_max']
178         logger.log('%s: setting max disk usage to %d KiB' % (self.name, disk_max))
179         try:  # if the sliver is over quota, .set_disk_limit will throw an exception
180             if not self.disk_usage_initialized:
181                 self.vm_running = False
182                 logger.log('%s: computing disk usage: beginning' % self.name)
183                 Sliver_VS._init_disk_info_sem.acquire()
184                 try: self.init_disk_info()
185                 finally: Sliver_VS._init_disk_info_sem.release()
186                 logger.log('%s: computing disk usage: ended' % self.name)
187                 self.disk_usage_initialized = True
188             vserver.VServer.set_disklimit(self, max(disk_max, self.disk_blocks))
189         except:
190             logger.log('%s: failed to set max disk usage' % self.name)
191             logger.log_exc(self.name)
192
193         # get/set the min/soft/hard values for all of the vserver
194         # related RLIMITS.  Note that vserver currently only
195         # implements support for hard limits.
196         for limit in vserver.RLIMITS.keys():
197             type = limit.lower()
198             minimum  = self.rspec['%s_min'%type]
199             soft = self.rspec['%s_soft'%type]
200             hard = self.rspec['%s_hard'%type]
201             update = self.set_rlimit(limit, hard, soft, minimum)
202             if update:
203                 logger.log('%s: setting rlimit %s to (%d, %d, %d)'
204                            % (self.name, type, hard, soft, minimum))
205
206         self.set_capabilities_config(self.rspec['capabilities'])
207         if self.rspec['capabilities']:
208             logger.log('%s: setting capabilities to %s' % (self.name, self.rspec['capabilities']))
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' % \
228                 (self.name, self.rspec['ip_addresses']))
229             self.set_ipaddresses_config(self.rspec['ip_addresses'])
230
231             if self.is_running():
232                 logger.log("%s: Setting name to %s" % (self.name, self.slice_id),2)
233                 self.setname(self.slice_id)
234  
235             if False: # Does not work properly yet.
236                 if self.have_limits_changed():
237                     logger.log('%s: limits have changed --- restarting' % self.name)
238                     stopcount = 10
239                     while self.is_running() and stopcount > 0:
240                         self.stop()
241                         delay = 1
242                         time.sleep(delay)
243                         stopcount = stopcount - 1
244                     self.start()
245
246         else:  # tell vsh to disable remote login by setting CPULIMIT to 0
247             logger.log('%s: disabling remote login' % self.name)
248             self.set_sched_config(0, 0)
249             self.stop()