Simple module for toggling network namespaces based on slice attributes
[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.log("%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.log("%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             # 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):
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 self.rspec['enabled'] > 0:
233             if cpu_pct > 0:
234                 logger.log('%s: setting cpu reservation to %d%%' % (self.name, cpu_pct))
235             else:
236                 cpu_pct = 0
237
238             if cpu_share > 0:
239                 logger.log('%s: setting cpu share to %d' % (self.name, cpu_share))
240             else:
241                 cpu_share = 0
242
243             self.set_sched_config(cpu_pct, cpu_share)
244             # if IP address isn't set (even to 0.0.0.0), sliver won't be able to use network
245             if self.rspec['ip_addresses'] != '0.0.0.0':
246                 logger.log('%s: setting IP address(es) to %s' % \
247                 (self.name, self.rspec['ip_addresses']))
248             self.set_ipaddresses_config(self.rspec['ip_addresses'])
249
250             if self.is_running():
251                 logger.log("%s: Setting name to %s" % (self.name, self.slice_id),2)
252                 self.setname(self.slice_id)
253  
254             if False: # Does not work properly yet.
255                 if self.have_limits_changed():
256                     logger.log('%s: limits have changed --- restarting' % self.name)
257                     stopcount = 10
258                     while self.is_running() and stopcount > 0:
259                         self.stop()
260                         delay = 1
261                         time.sleep(delay)
262                         stopcount = stopcount - 1
263                     self.start()
264
265         else:  # tell vsh to disable remote login by setting CPULIMIT to 0
266             logger.log('%s: disabling remote login' % self.name)
267             self.set_sched_config(0, 0)
268             self.stop()