typo
[nodemanager.git] / sliver_lxc.py
1 #
2
3 """LXC slivers"""
4
5 import subprocess
6 import sys
7 import time
8 import os, os.path
9 import grp
10 from pwd import getpwnam
11 from string import Template
12
13 # vsys probably should not be a plugin
14 # the thing is, the right way to handle stuff would be that
15 # if slivers get created by doing a,b,c
16 # then they sohuld be delted by doing c,b,a
17 # the current ordering model for vsys plugins completely fails to capture that
18 from plugins.vsys import removeSliverFromVsys, startService as vsysStartService
19
20 import libvirt
21
22 import logger
23 import plnode.bwlimit as bwlimit
24 from initscript import Initscript
25 from account import Account
26 from sliver_libvirt import Sliver_Libvirt
27
28 BTRFS_TIMEOUT=15*60
29
30 class Sliver_LXC(Sliver_Libvirt, Initscript):
31     """This class wraps LXC commands"""
32
33     SHELL = '/usr/sbin/vsh'
34     TYPE = 'sliver.LXC'
35     # Need to add a tag at myplc to actually use this account
36     # type = 'sliver.LXC'
37
38     REF_IMG_BASE_DIR = '/vservers/.lvref'
39     CON_BASE_DIR     = '/vservers'
40
41     def __init__ (self, rec):
42         name=rec['name']
43         Sliver_Libvirt.__init__ (self,rec)
44         Initscript.__init__ (self,name)
45
46     def configure (self, rec):
47         Sliver_Libvirt.configure (self,rec)
48
49         # in case we update nodemanager..
50         self.install_and_enable_vinit()
51         # do the configure part from Initscript
52         Initscript.configure(self,rec)
53
54     def start(self, delay=0):
55         if 'enabled' in self.rspec and self.rspec['enabled'] <= 0:
56             logger.log('sliver_lxc: not starting %s, is not enabled'%self.name)
57             return
58         # the generic /etc/init.d/vinit script is permanently refreshed, and enabled
59         self.install_and_enable_vinit()
60         # expose .ssh for omf_friendly slivers
61         if 'tags' in self.rspec and 'omf_control' in self.rspec['tags']:
62             Account.mount_ssh_dir(self.name)
63         Sliver_Libvirt.start (self, delay)
64         # if a change has occured in the slice initscript, reflect this in /etc/init.d/vinit.slice
65         self.refresh_slice_vinit()
66
67     def rerun_slice_vinit (self):
68         """This is called whenever the initscript code changes"""
69         # xxx - todo - not sure exactly how to:
70         # (.) invoke something in the guest
71         # (.) which options of systemctl should be used to trigger a restart
72         # should not prevent the first run from going fine hopefully
73         logger.log("WARNING: sliver_lxc.rerun_slice_vinit not implemented yet")
74
75     @staticmethod
76     def create(name, rec=None):
77         ''' Create dirs, copy fs image, lxc_create '''
78         logger.verbose ('sliver_lxc: %s create'%(name))
79         conn = Sliver_Libvirt.getConnection(Sliver_LXC.TYPE)
80
81         # Get the type of image from vref myplc tags specified as:
82         # pldistro = lxc
83         # fcdistro = squeeze
84         # arch x86_64
85
86         arch = 'x86_64'
87         tags = rec['rspec']['tags']
88         if 'arch' in tags:
89             arch = tags['arch']
90             if arch == 'i386':
91                 arch = 'i686'
92
93         vref = rec['vref']
94         if vref is None:
95             vref = "lxc-f18-x86_64"
96             logger.log("sliver_libvirt: %s: WARNING - no vref attached, using hard-wired default %s" % (name,vref))
97
98         refImgDir    = os.path.join(Sliver_LXC.REF_IMG_BASE_DIR, vref)
99         containerDir = os.path.join(Sliver_LXC.CON_BASE_DIR, name)
100
101         # check the template exists -- there's probably a better way..
102         if not os.path.isdir(refImgDir):
103             logger.log('sliver_lxc: %s: ERROR Could not create sliver - reference image %s not found' % (name,vref))
104             logger.log('sliver_lxc: %s: ERROR Expected reference image in %s'%(name,refImgDir))
105             return
106
107 # this hopefully should be fixed now
108 #        # in fedora20 we have some difficulty in properly cleaning up /vservers/<slicename>
109 #        # also note that running e.g. btrfs subvolume create /vservers/.lvref/image /vservers/foo
110 #        # behaves differently, whether /vservers/foo exists or not:
111 #        # if /vservers/foo does not exist, it creates /vservers/foo
112 #        # but if it does exist, then       it creates /vservers/foo/image !!
113 #        # so we need to check the expected container rootfs does not exist yet
114 #        # this hopefully could be removed in a future release 
115 #        if os.path.exists (containerDir):
116 #            logger.log("sliver_lxc: %s: WARNING cleaning up pre-existing %s"%(name,containerDir))
117 #            command = ['btrfs', 'subvolume', 'delete', containerDir]
118 #            logger.log_call(command, BTRFS_TIMEOUT)
119 #            # re-check
120 #            if os.path.exists (containerDir):
121 #                logger.log('sliver_lxc: %s: ERROR Could not create sliver - could not clean up empty %s'%(name,containerDir))
122 #                return
123
124         # Snapshot the reference image fs (assume the reference image is in its own
125         # subvolume)
126         command = ['btrfs', 'subvolume', 'snapshot', refImgDir, containerDir]
127         if not logger.log_call(command, timeout=BTRFS_TIMEOUT):
128             logger.log('sliver_lxc: ERROR Could not create BTRFS snapshot at', containerDir)
129             return
130         command = ['chmod', '755', containerDir]
131         logger.log_call(command)
132
133         # TODO: set quotas...
134
135         # Set hostname. A valid hostname cannot have '_'
136         #with open(os.path.join(containerDir, 'etc/hostname'), 'w') as f:
137         #    print >>f, name.replace('_', '-')
138
139         # Add slices group if not already present
140         try:
141             group = grp.getgrnam('slices')
142         except:
143             command = ['/usr/sbin/groupadd', 'slices']
144             logger.log_call(command)
145
146         # Add unix account (TYPE is specified in the subclass)
147         command = ['/usr/sbin/useradd', '-g', 'slices', '-s', Sliver_LXC.SHELL, name, '-p', '*']
148         logger.log_call(command)
149         command = ['mkdir', '/home/%s/.ssh'%name]
150         logger.log_call(command)
151
152         # Create PK pair keys to connect from the host to the guest without
153         # password... maybe remove the need for authentication inside the
154         # guest?
155         command = ['su', '-s', '/bin/bash', '-c', 'ssh-keygen -t rsa -N "" -f /home/%s/.ssh/id_rsa'%(name)]
156         logger.log_call(command)
157
158         command = ['chown', '-R', '%s.slices'%name, '/home/%s/.ssh'%name]
159         logger.log_call(command)
160
161         command = ['mkdir', '%s/root/.ssh'%containerDir]
162         logger.log_call(command)
163
164         command = ['cp', '/home/%s/.ssh/id_rsa.pub'%name, '%s/root/.ssh/authorized_keys'%containerDir]
165         logger.log_call(command)
166
167         logger.log("creating /etc/slicename file in %s" % os.path.join(containerDir,'etc/slicename'))
168         try:
169             file(os.path.join(containerDir,'etc/slicename'), 'w').write(name)
170         except:
171             logger.log_exc("exception while creating /etc/slicename")
172
173         try:
174             file(os.path.join(containerDir,'etc/slicefamily'), 'w').write(vref)
175         except:
176             logger.log_exc("exception while creating /etc/slicefamily")
177
178         uid = None
179         try:
180             uid = getpwnam(name).pw_uid
181         except KeyError:
182             # keyerror will happen if user id was not created successfully
183             logger.log_exc("exception while getting user id")
184
185         if uid is not None:
186             logger.log("uid is %d" % uid)
187             command = ['mkdir', '%s/home/%s' % (containerDir, name)]
188             logger.log_call(command)
189             command = ['chown', name, '%s/home/%s' % (containerDir, name)]
190             logger.log_call(command)
191             etcpasswd = os.path.join(containerDir, 'etc/passwd')
192             etcgroup = os.path.join(containerDir, 'etc/group')
193             if os.path.exists(etcpasswd):
194                 # create all accounts with gid=1001 - i.e. 'slices' like it is in the root context
195                 slices_gid=1001
196                 logger.log("adding user %(name)s id %(uid)d gid %(slices_gid)d to %(etcpasswd)s" % (locals()))
197                 try:
198                     file(etcpasswd,'a').write("%(name)s:x:%(uid)d:%(slices_gid)d::/home/%(name)s:/bin/bash\n" % locals())
199                 except:
200                     logger.log_exc("exception while updating %s"%etcpasswd)
201                 logger.log("adding group slices with gid %(slices_gid)d to %(etcgroup)s"%locals())
202                 try:
203                     file(etcgroup,'a').write("slices:x:%(slices_gid)d\n"%locals())
204                 except:
205                     logger.log_exc("exception while updating %s"%etcgroup)
206             sudoers = os.path.join(containerDir, 'etc/sudoers')
207             if os.path.exists(sudoers):
208                 try:
209                     file(sudoers,'a').write("%s ALL=(ALL) NOPASSWD: ALL\n" % name)
210                 except:
211                     logger.log_exc("exception while updating /etc/sudoers")
212
213         # customizations for the user environment - root or slice uid
214         # we save the whole business in /etc/planetlab.profile 
215         # and source this file for both root and the slice uid's .profile
216         # prompt for slice owner, + LD_PRELOAD for transparently wrap bind
217         pl_profile=os.path.join(containerDir,"etc/planetlab.profile")
218         ld_preload_text="""# by default, we define this setting so that calls to bind(2),
219 # when invoked on 0.0.0.0, get transparently redirected to the public interface of this node
220 # see https://svn.planet-lab.org/wiki/LxcPortForwarding"""
221         usrmove_path_text="""# VM's before Features/UsrMove need /bin and /sbin in their PATH"""
222         usrmove_path_code="""
223 pathmunge () {
224         if ! echo $PATH | /bin/egrep -q "(^|:)$1($|:)" ; then
225            if [ "$2" = "after" ] ; then
226               PATH=$PATH:$1
227            else
228               PATH=$1:$PATH
229            fi
230         fi
231 }
232 pathmunge /bin after
233 pathmunge /sbin after
234 unset pathmunge
235 """
236         with open(pl_profile,'w') as f:
237             f.write("export PS1='%s@\H \$ '\n"%(name))
238             f.write("%s\n"%ld_preload_text)
239             f.write("export LD_PRELOAD=/etc/planetlab/lib/bind_public.so\n")
240             f.write("%s\n"%usrmove_path_text)
241             f.write("%s\n"%usrmove_path_code)
242
243         # make sure this file is sourced from both root's and slice's .profile
244         enforced_line = "[ -f /etc/planetlab.profile ] && source /etc/planetlab.profile\n"
245         for path in [ 'root/.profile', 'home/%s/.profile'%name ]:
246             from_root=os.path.join(containerDir,path)
247             # if dir is not yet existing let's forget it for now
248             if not os.path.isdir(os.path.dirname(from_root)): continue
249             found=False
250             try: 
251                 contents=file(from_root).readlines()
252                 for content in contents:
253                     if content==enforced_line: found=True
254             except IOError: pass
255             if not found:
256                 with open(from_root,"a") as user_profile:
257                     user_profile.write(enforced_line)
258                 # in case we create the slice's .profile when writing
259                 if from_root.find("/home")>=0:
260                     command=['chown','%s:slices'%name,from_root]
261                     logger.log_call(command)
262
263         # Lookup for xid and create template after the user is created so we
264         # can get the correct xid based on the name of the slice
265         xid = bwlimit.get_xid(name)
266
267         # Template for libvirt sliver configuration
268         template_filename_sliceimage = os.path.join(Sliver_LXC.REF_IMG_BASE_DIR,'lxc_template.xml')
269         if os.path.isfile (template_filename_sliceimage):
270             logger.log("WARNING: using compat template %s"%template_filename_sliceimage)
271             template_filename=template_filename_sliceimage
272         else:
273             logger.log("Cannot find XML template %s"%template_filename_sliceimage)
274             return
275
276         interfaces = Sliver_Libvirt.get_interfaces_xml(rec)
277
278         try:
279             with open(template_filename) as f:
280                 template = Template(f.read())
281                 xml  = template.substitute(name=name, xid=xid, interfaces=interfaces, arch=arch)
282         except IOError:
283             logger.log('Failed to parse or use XML template file %s'%template_filename)
284             return
285
286         # Lookup for the sliver before actually
287         # defining it, just in case it was already defined.
288         try:
289             dom = conn.lookupByName(name)
290         except:
291             dom = conn.defineXML(xml)
292         logger.verbose('lxc_create: %s -> %s'%(name, Sliver_Libvirt.dom_details(dom)))
293
294
295     @staticmethod
296     def destroy(name):
297         # umount .ssh directory - only if mounted
298         Account.umount_ssh_dir(name)
299         logger.verbose ('sliver_lxc: %s destroy'%(name))
300         conn = Sliver_Libvirt.getConnection(Sliver_LXC.TYPE)
301
302         containerDir = Sliver_LXC.CON_BASE_DIR + '/%s'%(name)
303
304         try:
305             # Destroy libvirt domain
306             dom = conn.lookupByName(name)
307         except:
308             logger.verbose('sliver_lxc.destroy: Domain %s does not exist!' % name)
309             return
310
311         # Slivers with vsys running will fail the subvolume delete
312         # removeSliverFromVsys return True if it stops vsys, telling us to start it again later
313         vsys_stopped = removeSliverFromVsys (name)
314
315         try:
316             logger.log("sliver_lxc.destroy: destroying domain %s"%name)
317             dom.destroy()
318         except:
319             logger.verbose('sliver_lxc.destroy: Domain %s not running... continuing.' % name)
320
321         try:
322             logger.log("sliver_lxc.destroy: undefining domain %s"%name)
323             dom.undefine()
324         except:
325             logger.verbose('sliver_lxc.destroy: Domain %s is not defined... continuing.' % name)
326
327         # Remove user after destroy domain to force logout
328         command = ['/usr/sbin/userdel', '-f', '-r', name]
329         logger.log_call(command)
330
331         # Remove rootfs of destroyed domain
332         command = ['btrfs', 'subvolume', 'delete', containerDir]
333         logger.log_call(command, timeout=BTRFS_TIMEOUT)
334         
335         # For some reason I am seeing this :
336         #log_call: running command btrfs subvolume delete /vservers/inri_sl1
337         #log_call: ERROR: cannot delete '/vservers/inri_sl1' - Device or resource busy
338         #log_call: Delete subvolume '/vservers/inri_sl1'
339         #log_call:end command (btrfs subvolume delete /vservers/inri_sl1) returned with code 1
340         #
341         # something must have an open handle to a file in there, but I can't find out what it is
342         # the following code aims at gathering data on what is going on in the system at this point in time
343         # note that some time later (typically when the sliver gets re-created) the same
344         # attempt at deleting the subvolume does work
345         # also lsof never shows anything relevant; this is painful..
346
347         if not os.path.exists(containerDir):
348             logger.log('sliver_lxc.destroy: %s cleanly destroyed.'%name)
349         else:
350             # we're in /
351             #logger.log("-TMP-cwd %s : %s"%(name,os.getcwd()))
352             # also lsof never shows anything relevant; this is painful..
353             #logger.log("-TMP-lsof %s"%name)
354             #command=['lsof']
355             #logger.log_call(command)
356             logger.log("-TMP-ls-l %s"%name)
357             command = ['ls', '-l', containerDir]
358             logger.log_call(command)
359             if os.path.exists(containerDir):
360                 logger.log('sliver_lxc.destroy: ERROR could not cleanly destroy %s - giving up'%name)
361
362         if vsys_stopped: vsysStartService()