ee0b5d498876f31ba05c2718d0df304833ccf16a
[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): vserver.VServer.is_running(self)
192
193     def set_resources(self):
194         disk_max = self.rspec['disk_max']
195         logger.log('%s: setting max disk usage to %d KiB' % (self.name, disk_max))
196         try:  # if the sliver is over quota, .set_disk_limit will throw an exception
197             if not self.disk_usage_initialized:
198                 self.vm_running = False
199                 Sliver_VS._init_disk_info_sem.acquire()
200                 logger.log('%s: computing disk usage: beginning' % self.name)
201                 try: self.init_disk_info()
202                 finally: Sliver_VS._init_disk_info_sem.release()
203                 logger.log('%s: computing disk usage: ended' % self.name)
204                 self.disk_usage_initialized = True
205             vserver.VServer.set_disklimit(self, max(disk_max, self.disk_blocks))
206         except:
207             logger.log('%s: failed to set max disk usage' % self.name)
208             logger.log_exc(self.name)
209
210         # get/set the min/soft/hard values for all of the vserver
211         # related RLIMITS.  Note that vserver currently only
212         # implements support for hard limits.
213         for limit in vserver.RLIMITS.keys():
214             type = limit.lower()
215             minimum  = self.rspec['%s_min'%type]
216             soft = self.rspec['%s_soft'%type]
217             hard = self.rspec['%s_hard'%type]
218             update = self.set_rlimit(limit, hard, soft, minimum)
219             if update:
220                 logger.log('%s: setting rlimit %s to (%d, %d, %d)'
221                            % (self.name, type, hard, soft, minimum))
222
223         self.set_capabilities_config(self.rspec['capabilities'])
224         if self.rspec['capabilities']:
225             logger.log('%s: setting capabilities to %s' % (self.name, self.rspec['capabilities']))
226
227         cpu_pct = self.rspec['cpu_pct']
228         cpu_share = self.rspec['cpu_share']
229
230         if self.rspec['enabled'] > 0:
231             if cpu_pct > 0:
232                 logger.log('%s: setting cpu reservation to %d%%' % (self.name, cpu_pct))
233             else:
234                 cpu_pct = 0
235
236             if cpu_share > 0:
237                 logger.log('%s: setting cpu share to %d' % (self.name, cpu_share))
238             else:
239                 cpu_share = 0
240
241             self.set_sched_config(cpu_pct, cpu_share)
242             # if IP address isn't set (even to 0.0.0.0), sliver won't be able to use network
243             if self.rspec['ip_addresses'] != '0.0.0.0':
244                 logger.log('%s: setting IP address(es) to %s' % \
245                 (self.name, self.rspec['ip_addresses']))
246             self.set_ipaddresses_config(self.rspec['ip_addresses'])
247
248             if self.is_running():
249                 logger.log("%s: Setting name to %s" % (self.name, self.slice_id),2)
250                 self.setname(self.slice_id)
251  
252             if False: # Does not work properly yet.
253                 if self.have_limits_changed():
254                     logger.log('%s: limits have changed --- restarting' % self.name)
255                     stopcount = 10
256                     while self.is_running() and stopcount > 0:
257                         self.stop()
258                         delay = 1
259                         time.sleep(delay)
260                         stopcount = stopcount - 1
261                     self.start()
262
263         else:  # tell vsh to disable remote login by setting CPULIMIT to 0
264             logger.log('%s: disabling remote login' % self.name)
265             self.set_sched_config(0, 0)
266             self.stop()