Merge from Branch.
[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.configure(rec)
72
73     @staticmethod
74     def create(name, vref = None):
75         logger.verbose('Sliver_VS:create - name=%s'%name)
76         if vref is None:
77             vref='default'
78         try:
79             ### locating the right slicefamily
80             # this is a first draft, and more a proof of concept thing
81             # the idea is to parse vref for dash-separated wishes,
82             # and to project these against the defaults
83             # so e.g. if the default slice family (as found in /etc/planetlab/slicefamily)
84             # is planetlab-f8-i386, then here is what we get
85             # vref=x86_64             -> vuseradd -t planetlab-f8-x86_64 
86             # vref=centos5            -> vuseradd -t planetlab-centos5-i386 
87             # vref=centos5-onelab     -> vuseradd -t onelab-centos5-i386 
88             # vref=planetflow         -> vuseradd -t planetflow-f8-i386
89             # vref=x86_64-planetflow  -> vuseradd -t planetflow-f8-x86_64
90
91             # default
92             default=file("/etc/planetlab/slicefamily").read().strip()
93             (pldistro,fcdistro,arch) = default.split("-")
94
95             known_archs = [ 'i386', 'x86_64' ]
96             known_fcdistros = [ 'f8', 'f9', 'centos5' ]
97             # from the slice attribute: cut dashes and try to figure the meaning
98             slice_wishes = vref.split("-")
99             for wish in slice_wishes:
100                 if wish in known_archs:
101                     arch=wish
102                 elif wish in known_fcdistros:
103                     fcdistro=wish
104                 else:
105                     pldistro=wish
106
107             # rejoin the parts
108             refname="-".join( (pldistro,fcdistro,arch) )
109
110             # check the template exists -- there's probably a better way..
111             if not os.path.isdir ("/vservers/.vref/%s"%refname):
112                 logger.log("%s (%s) : vref %s not found, using default %s"%(
113                         name,vref,refname,default))
114                 refname=default
115                 # reset so arch is right
116                 (pldistro,fcdistro,arch) = default.split("-")
117                 # could check again, but as we have /etc/slicefamily 
118                 # there's probably no /vservers/.vref/default
119
120         except IOError:
121             # have not found slicefamily
122             logger.log("%s (%s): legacy node - using fallback vrefname 'default'"%(name,vref))
123             # for legacy nodes
124             refname="default"
125             arch="i386"
126         except:
127             logger.log("%s (%s) : unexpected error follows - using 'default'"%(name,vref))
128             logger.log(traceback.format_exc())
129             refname="default"
130             arch="i386"
131             
132         def personality (arch):
133             personality="linux32"
134             if arch.find("64")>=0:
135                 personality="linux64"
136             return personality
137
138         logger.log_call('/usr/sbin/vuseradd', '-t', refname, name)
139         # export slicename to the slice in /etc/slicename
140         file('/vservers/%s/etc/slicename' % name, 'w').write(name)
141         # set personality: only if needed (if arch's differ)
142         if tools.root_context_arch() != arch:
143             file('/etc/vservers/%s/personality' % name, 'w').write(personality(arch))
144             logger.log('%s: set personality to %s'%(name,personality(arch)))
145
146     @staticmethod
147     def destroy(name): logger.log_call('/usr/sbin/vuserdel', name)
148
149     def configure(self, rec):
150         new_rspec = rec['_rspec']
151         if new_rspec != self.rspec:
152             self.rspec = new_rspec
153             self.set_resources()
154
155         new_initscript = rec['initscript']
156         if new_initscript != self.initscript:
157             self.initscript = new_initscript
158             self.initscriptchanged = True
159
160         accounts.Account.configure(self, rec)  # install ssh keys
161
162     def start(self, delay=0):
163         if self.rspec['enabled'] > 0:
164             logger.log('%s: starting in %d seconds' % (self.name, delay))
165             time.sleep(delay)
166             # VServer.start calls fork() internally, 
167             # so just close the nonstandard fds and fork once to avoid creating zombies
168             child_pid = os.fork()
169             if child_pid == 0:
170                 if self.initscriptchanged:
171                     logger.log('%s: installing initscript' % self.name)
172                     def install_initscript():
173                         flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
174                         fd = os.open('/etc/rc.vinit', flags, 0755)
175                         os.write(fd, self.initscript)
176                         os.close(fd)
177                     try:
178                         self.chroot_call(install_initscript)
179                     except: logger.log_exc(self.name)
180                 tools.close_nonstandard_fds()
181                 vserver.VServer.start(self)
182                 os._exit(0)
183             else: 
184                 os.waitpid(child_pid, 0)
185                 self.initscriptchanged = False
186         else: logger.log('%s: not starting, is not enabled' % self.name)
187
188     def stop(self):
189         logger.log('%s: stopping' % self.name)
190         vserver.VServer.stop(self)
191
192     def is_running(self): 
193         return vserver.VServer.is_running(self)
194
195     def set_resources(self,setup=False):
196         disk_max = self.rspec['disk_max']
197         logger.log('%s: setting max disk usage to %d KiB' % (self.name, disk_max))
198         try:  # if the sliver is over quota, .set_disk_limit will throw an exception
199             if not self.disk_usage_initialized:
200                 self.vm_running = False
201                 Sliver_VS._init_disk_info_sem.acquire()
202                 logger.log('%s: computing disk usage: beginning' % self.name)
203                 try: self.init_disk_info()
204                 finally: Sliver_VS._init_disk_info_sem.release()
205                 logger.log('%s: computing disk usage: ended' % self.name)
206                 self.disk_usage_initialized = True
207             vserver.VServer.set_disklimit(self, max(disk_max, self.disk_blocks))
208         except:
209             logger.log('%s: failed to set max disk usage' % self.name)
210             logger.log_exc(self.name)
211
212         # get/set the min/soft/hard values for all of the vserver
213         # related RLIMITS.  Note that vserver currently only
214         # implements support for hard limits.
215         for limit in vserver.RLIMITS.keys():
216             type = limit.lower()
217             minimum  = self.rspec['%s_min'%type]
218             soft = self.rspec['%s_soft'%type]
219             hard = self.rspec['%s_hard'%type]
220             update = self.set_rlimit(limit, hard, soft, minimum)
221             if update:
222                 logger.log('%s: setting rlimit %s to (%d, %d, %d)'
223                            % (self.name, type, hard, soft, minimum))
224
225         self.set_capabilities_config(self.rspec['capabilities'])
226         if self.rspec['capabilities']:
227             logger.log('%s: setting capabilities to %s' % (self.name, self.rspec['capabilities']))
228
229         cpu_pct = self.rspec['cpu_pct']
230         cpu_share = self.rspec['cpu_share']
231
232         if setup:
233             for key in self.rspec.keys():
234                 if key.find('sysctl.') == 0:
235                     sysctl=key.split('.')
236                     try:
237                         path="/proc/sys/%s" % ("/".join(sysctl[1:]))
238                         logger.log("%s: opening %s"%(self.name,path))
239                         flags = os.O_WRONLY
240                         fd = os.open(path, flags)
241                         logger.log("%s: writing %s=%s"%(self.name,key,self.rspec[key]))
242                         os.write(fd,self.rspec[key])
243                         os.close(fd)
244                     except IOError, e:
245                         logger.log("%s: could not set %s=%s"%(self.name,key,self.rspec[key]))
246                         logger.log("%s: error = %s"%(self.name,e))
247
248
249         if self.rspec['enabled'] > 0:
250             if cpu_pct > 0:
251                 logger.log('%s: setting cpu reservation to %d%%' % (self.name, cpu_pct))
252             else:
253                 cpu_pct = 0
254
255             if cpu_share > 0:
256                 logger.log('%s: setting cpu share to %d' % (self.name, cpu_share))
257             else:
258                 cpu_share = 0
259
260             self.set_sched_config(cpu_pct, cpu_share)
261             # if IP address isn't set (even to 0.0.0.0), sliver won't be able to use network
262             if self.rspec['ip_addresses'] != '0.0.0.0':
263                 logger.log('%s: setting IP address(es) to %s' % \
264                 (self.name, self.rspec['ip_addresses']))
265             self.set_ipaddresses_config(self.rspec['ip_addresses'])
266
267             if self.is_running():
268                 logger.log("%s: Setting name to %s" % (self.name, self.slice_id),2)
269                 self.setname(self.slice_id)
270  
271             if False: # Does not work properly yet.
272                 if self.have_limits_changed():
273                     logger.log('%s: limits have changed --- restarting' % self.name)
274                     stopcount = 10
275                     while self.is_running() and stopcount > 0:
276                         self.stop()
277                         delay = 1
278                         time.sleep(delay)
279                         stopcount = stopcount - 1
280                     self.start()
281
282         else:  # tell vsh to disable remote login by setting CPULIMIT to 0
283             logger.log('%s: disabling remote login' % self.name)
284             self.set_sched_config(0, 0)
285             self.stop()