376bf939414c72efda9b658971878d2c8fd2729c
[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
23 import vserver
24
25 import accounts
26 import logger
27 import tools
28 from threading import BoundedSemaphore
29
30 globalsem = BoundedSemaphore()
31
32 # special constant that tells vserver to keep its existing settings
33 KEEP_LIMIT = vserver.VC_LIM_KEEP
34
35 # populate the sliver/vserver specific default allocations table,
36 # which is used to look for slice attributes
37 DEFAULT_ALLOCATION = {}
38 for rlimit in vserver.RLIMITS.keys():
39     rlim = rlimit.lower()
40     DEFAULT_ALLOCATION["%s_min"%rlim]=KEEP_LIMIT
41     DEFAULT_ALLOCATION["%s_soft"%rlim]=KEEP_LIMIT
42     DEFAULT_ALLOCATION["%s_hard"%rlim]=KEEP_LIMIT
43
44 class Sliver_VS(accounts.Account, vserver.VServer):
45     """This class wraps vserver.VServer to make its interface closer to what we need."""
46
47     SHELL = '/bin/vsh'
48     TYPE = 'sliver.VServer'
49     _init_disk_info_sem = globalsem
50
51     def __init__(self, rec):
52         logger.verbose ('initing Sliver_VS with name=%s'%rec['name'])
53         try:
54             vserver.VServer.__init__(self, rec['name'],logfile='/var/log/nm')
55         except Exception, err:
56             if not isinstance(err, vserver.NoSuchVServer):
57                 # Probably a bad vserver or vserver configuration file
58                 logger.log_exc(self.name)
59                 logger.log('%s: recreating bad vserver' % rec['name'])
60                 self.destroy(rec['name'])
61             self.create(rec['name'], rec['vref'])
62             vserver.VServer.__init__(self, rec['name'],logfile='/var/log/nm')
63
64         self.keys = ''
65         self.rspec = {}
66         self.initscript = ''
67         self.slice_id = rec['slice_id']
68         self.disk_usage_initialized = False
69         self.initscriptchanged = False
70         self.configure(rec)
71
72     @staticmethod
73     def create(name, vref = None):
74         logger.verbose('Sliver_VS:create - name=%s'%name)
75         if vref is None:
76             vref='default'
77         try:
78             ### locating the right slicefamily
79             # this is a first draft, and more a proof of concept thing
80             # the idea is to parse vref for dash-separated wishes,
81             # and to project these against the defaults
82             # so e.g. if the default slice family (as found in /etc/planetlab/slicefamily)
83             # is planetlab-f8-i386, then here is what we get
84             # vref=x86_64             -> vuseradd -t planetlab-f8-x86_64 
85             # vref=centos5            -> vuseradd -t planetlab-centos5-i386 
86             # vref=centos5-onelab     -> vuseradd -t onelab-centos5-i386 
87             # vref=planetflow         -> vuseradd -t planetflow-f8-i386
88             # vref=x86_64-planetflow  -> vuseradd -t planetflow-f8-x86_64
89
90             # default
91             default=file("/etc/planetlab/slicefamily").read().strip()
92             (pldistro,fcdistro,arch) = default.split("-")
93
94             known_archs = [ 'i386', 'x86_64' ]
95             known_fcdistros = [ 'f8', 'f9', 'centos5' ]
96             # from the slice attribute: cut dashes and try to figure the meaning
97             slice_wishes = vref.split("-")
98             for wish in slice_wishes:
99                 if wish in known_archs:
100                     arch=wish
101                 elif wish in known_fcdistros:
102                     fcdistro=wish
103                 else:
104                     pldistro=wish
105
106             # rejoin the parts
107             refname="-".join( (pldistro,fcdistro,arch) )
108
109             # check the template exists -- there's probably a better way..
110             if not os.path.isdir ("/vservers/.vref/%s"%refname):
111                 logger.verbose("%s (%s) : vref %s not found, using default %s"%(
112                         name,vref,refname,default))
113                 refname=default
114                 # reset so arch is right
115                 (pldistro,fcdistro,arch) = default.split("-")
116                 # could check again, but as we have /etc/slicefamily 
117                 # there's probably no /vservers/.vref/default
118
119         except IOError:
120             # have not found slicefamily
121             logger.verbose("%s (%s): legacy node - using fallback vrefname 'default'"%(name,vref))
122             # for legacy nodes
123             refname="default"
124             arch="i386"
125         except:
126             import traceback
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             child_pid = os.fork()
167             if child_pid == 0:
168                 if self.initscriptchanged:
169                     logger.log('%s: installing initscript' % self.name)
170                     def install_initscript():
171                         flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
172                         fd = os.open('/etc/rc.vinit', flags, 0755)
173                         os.write(fd, self.initscript)
174                         os.close(fd)
175                     try:
176                         self.chroot_call(install_initscript)
177                         self.initscriptchanged = False
178                     except: logger.log_exc(self.name)
179                 # VServer.start calls fork() internally, 
180                 # so just close the nonstandard fds and fork once to avoid creating zombies
181                 tools.close_nonstandard_fds()
182                 vserver.VServer.start(self)
183                 os._exit(0)
184             else: os.waitpid(child_pid, 0)
185         else: logger.log('%s: not starting, is not enabled' % self.name)
186
187     def stop(self):
188         logger.log('%s: stopping' % self.name)
189         vserver.VServer.stop(self)
190
191     def is_running(self): 
192         return vserver.VServer.is_running(self)
193
194     def set_resources(self):
195         disk_max = self.rspec['disk_max']
196         logger.log('%s: setting max disk usage to %d KiB' % (self.name, disk_max))
197         try:  # if the sliver is over quota, .set_disk_limit will throw an exception
198             if not self.disk_usage_initialized:
199                 self.vm_running = False
200                 Sliver_VS._init_disk_info_sem.acquire()
201                 logger.log('%s: computing disk usage: beginning' % self.name)
202                 try: self.init_disk_info()
203                 finally: Sliver_VS._init_disk_info_sem.release()
204                 logger.log('%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('%s: failed to set max disk usage' % self.name)
209             logger.log_exc(self.name)
210
211         # get/set the min/soft/hard values for all of the vserver
212         # related RLIMITS.  Note that vserver currently only
213         # implements support for hard limits.
214         for limit in vserver.RLIMITS.keys():
215             type = limit.lower()
216             minimum  = self.rspec['%s_min'%type]
217             soft = self.rspec['%s_soft'%type]
218             hard = self.rspec['%s_hard'%type]
219             update = self.set_rlimit(limit, hard, soft, minimum)
220             if update:
221                 logger.log('%s: setting rlimit %s to (%d, %d, %d)'
222                            % (self.name, type, hard, soft, minimum))
223
224         self.set_capabilities_config(self.rspec['capabilities'])
225         if self.rspec['capabilities']:
226             logger.log('%s: setting capabilities to %s' % (self.name, self.rspec['capabilities']))
227
228         cpu_pct = self.rspec['cpu_pct']
229         cpu_share = self.rspec['cpu_share']
230
231         if self.rspec['enabled'] > 0:
232             if cpu_pct > 0:
233                 logger.log('%s: setting cpu reservation to %d%%' % (self.name, cpu_pct))
234             else:
235                 cpu_pct = 0
236
237             if cpu_share > 0:
238                 logger.log('%s: setting cpu share to %d' % (self.name, cpu_share))
239             else:
240                 cpu_share = 0
241
242             self.set_sched_config(cpu_pct, cpu_share)
243             # if IP address isn't set (even to 0.0.0.0), sliver won't be able to use network
244             if self.rspec['ip_addresses'] != '0.0.0.0':
245                 logger.log('%s: setting IP address(es) to %s' % \
246                 (self.name, self.rspec['ip_addresses']))
247             self.set_ipaddresses_config(self.rspec['ip_addresses'])
248
249             if self.is_running():
250                 logger.log("%s: Setting name to %s" % (self.name, self.slice_id),2)
251                 self.setname(self.slice_id)
252  
253             if False: # Does not work properly yet.
254                 if self.have_limits_changed():
255                     logger.log('%s: limits have changed --- restarting' % self.name)
256                     stopcount = 10
257                     while self.is_running() and stopcount > 0:
258                         self.stop()
259                         delay = 1
260                         time.sleep(delay)
261                         stopcount = stopcount - 1
262                     self.start()
263
264         else:  # tell vsh to disable remote login by setting CPULIMIT to 0
265             logger.log('%s: disabling remote login' % self.name)
266             self.set_sched_config(0, 0)
267             self.stop()