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