do not call lsof, this is useless and produces huge logs
[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         # in fedora20 we have some difficulty in properly cleaning up /vservers/<slicename>
108         # also note that running e.g. btrfs subvolume create /vservers/.lvref/image /vservers/foo
109         # behaves differently, whether /vservers/foo exists or not:
110         # if /vservers/foo does not exist, it creates /vservers/foo
111         # but if it does exist, then       it creates /vservers/foo/image !!
112         # so we need to check the expected container rootfs does not exist yet
113         # this hopefully could be removed in a future release 
114         if os.path.exists (containerDir):
115             logger.log("sliver_lxc: %s: WARNING cleaning up pre-existing %s"%(name,containerDir))
116             command = ['btrfs', 'subvolume', 'delete', containerDir]
117             logger.log_call(command, BTRFS_TIMEOUT)
118             # re-check
119             if os.path.exists (containerDir):
120                 logger.log('sliver_lxc: %s: ERROR Could not create sliver - could not clean up empty %s'%(name,containerDir))
121                 return
122
123         # Snapshot the reference image fs (assume the reference image is in its own
124         # subvolume)
125         command = ['btrfs', 'subvolume', 'snapshot', refImgDir, containerDir]
126         if not logger.log_call(command, timeout=BTRFS_TIMEOUT):
127             logger.log('sliver_lxc: ERROR Could not create BTRFS snapshot at', containerDir)
128             return
129         command = ['chmod', '755', containerDir]
130         logger.log_call(command)
131
132         # TODO: set quotas...
133
134         # Set hostname. A valid hostname cannot have '_'
135         #with open(os.path.join(containerDir, 'etc/hostname'), 'w') as f:
136         #    print >>f, name.replace('_', '-')
137
138         # Add slices group if not already present
139         try:
140             group = grp.getgrnam('slices')
141         except:
142             command = ['/usr/sbin/groupadd', 'slices']
143             logger.log_call(command)
144
145         # Add unix account (TYPE is specified in the subclass)
146         command = ['/usr/sbin/useradd', '-g', 'slices', '-s', Sliver_LXC.SHELL, name, '-p', '*']
147         logger.log_call(command)
148         command = ['mkdir', '/home/%s/.ssh'%name]
149         logger.log_call(command)
150
151         # Create PK pair keys to connect from the host to the guest without
152         # password... maybe remove the need for authentication inside the
153         # guest?
154         command = ['su', '-s', '/bin/bash', '-c', 'ssh-keygen -t rsa -N "" -f /home/%s/.ssh/id_rsa'%(name)]
155         logger.log_call(command)
156
157         command = ['chown', '-R', '%s.slices'%name, '/home/%s/.ssh'%name]
158         logger.log_call(command)
159
160         command = ['mkdir', '%s/root/.ssh'%containerDir]
161         logger.log_call(command)
162
163         command = ['cp', '/home/%s/.ssh/id_rsa.pub'%name, '%s/root/.ssh/authorized_keys'%containerDir]
164         logger.log_call(command)
165
166         logger.log("creating /etc/slicename file in %s" % os.path.join(containerDir,'etc/slicename'))
167         try:
168             file(os.path.join(containerDir,'etc/slicename'), 'w').write(name)
169         except:
170             logger.log_exc("exception while creating /etc/slicename")
171
172         try:
173             file(os.path.join(containerDir,'etc/slicefamily'), 'w').write(vref)
174         except:
175             logger.log_exc("exception while creating /etc/slicefamily")
176
177         uid = None
178         try:
179             uid = getpwnam(name).pw_uid
180         except KeyError:
181             # keyerror will happen if user id was not created successfully
182             logger.log_exc("exception while getting user id")
183
184         if uid is not None:
185             logger.log("uid is %d" % uid)
186             command = ['mkdir', '%s/home/%s' % (containerDir, name)]
187             logger.log_call(command)
188             command = ['chown', name, '%s/home/%s' % (containerDir, name)]
189             logger.log_call(command)
190             etcpasswd = os.path.join(containerDir, 'etc/passwd')
191             etcgroup = os.path.join(containerDir, 'etc/group')
192             if os.path.exists(etcpasswd):
193                 # create all accounts with gid=1001 - i.e. 'slices' like it is in the root context
194                 slices_gid=1001
195                 logger.log("adding user %(name)s id %(uid)d gid %(slices_gid)d to %(etcpasswd)s" % (locals()))
196                 try:
197                     file(etcpasswd,'a').write("%(name)s:x:%(uid)d:%(slices_gid)d::/home/%(name)s:/bin/bash\n" % locals())
198                 except:
199                     logger.log_exc("exception while updating %s"%etcpasswd)
200                 logger.log("adding group slices with gid %(slices_gid)d to %(etcgroup)s"%locals())
201                 try:
202                     file(etcgroup,'a').write("slices:x:%(slices_gid)d\n"%locals())
203                 except:
204                     logger.log_exc("exception while updating %s"%etcgroup)
205             sudoers = os.path.join(containerDir, 'etc/sudoers')
206             if os.path.exists(sudoers):
207                 try:
208                     file(sudoers,'a').write("%s ALL=(ALL) NOPASSWD: ALL\n" % name)
209                 except:
210                     logger.log_exc("exception while updating /etc/sudoers")
211
212         # customizations for the user environment - root or slice uid
213         # we save the whole business in /etc/planetlab.profile 
214         # and source this file for both root and the slice uid's .profile
215         # prompt for slice owner, + LD_PRELOAD for transparently wrap bind
216         pl_profile=os.path.join(containerDir,"etc/planetlab.profile")
217         ld_preload_text="""# by default, we define this setting so that calls to bind(2),
218 # when invoked on 0.0.0.0, get transparently redirected to the public interface of this node
219 # see https://svn.planet-lab.org/wiki/LxcPortForwarding"""
220         usrmove_path_text="""# VM's before Features/UsrMove need /bin and /sbin in their PATH"""
221         usrmove_path_code="""
222 pathmunge () {
223         if ! echo $PATH | /bin/egrep -q "(^|:)$1($|:)" ; then
224            if [ "$2" = "after" ] ; then
225               PATH=$PATH:$1
226            else
227               PATH=$1:$PATH
228            fi
229         fi
230 }
231 pathmunge /bin after
232 pathmunge /sbin after
233 unset pathmunge
234 """
235         with open(pl_profile,'w') as f:
236             f.write("export PS1='%s@\H \$ '\n"%(name))
237             f.write("%s\n"%ld_preload_text)
238             f.write("export LD_PRELOAD=/etc/planetlab/lib/bind_public.so\n")
239             f.write("%s\n"%usrmove_path_text)
240             f.write("%s\n"%usrmove_path_code)
241
242         # make sure this file is sourced from both root's and slice's .profile
243         enforced_line = "[ -f /etc/planetlab.profile ] && source /etc/planetlab.profile\n"
244         for path in [ 'root/.profile', 'home/%s/.profile'%name ]:
245             from_root=os.path.join(containerDir,path)
246             # if dir is not yet existing let's forget it for now
247             if not os.path.isdir(os.path.dirname(from_root)): continue
248             found=False
249             try: 
250                 contents=file(from_root).readlines()
251                 for content in contents:
252                     if content==enforced_line: found=True
253             except IOError: pass
254             if not found:
255                 with open(from_root,"a") as user_profile:
256                     user_profile.write(enforced_line)
257                 # in case we create the slice's .profile when writing
258                 if from_root.find("/home")>=0:
259                     command=['chown','%s:slices'%name,from_root]
260                     logger.log_call(command)
261
262         # Lookup for xid and create template after the user is created so we
263         # can get the correct xid based on the name of the slice
264         xid = bwlimit.get_xid(name)
265
266         # Template for libvirt sliver configuration
267         template_filename_sliceimage = os.path.join(Sliver_LXC.REF_IMG_BASE_DIR,'lxc_template.xml')
268         if os.path.isfile (template_filename_sliceimage):
269             logger.log("WARNING: using compat template %s"%template_filename_sliceimage)
270             template_filename=template_filename_sliceimage
271         else:
272             logger.log("Cannot find XML template %s"%template_filename_sliceimage)
273             return
274
275         interfaces = Sliver_Libvirt.get_interfaces_xml(rec)
276
277         try:
278             with open(template_filename) as f:
279                 template = Template(f.read())
280                 xml  = template.substitute(name=name, xid=xid, interfaces=interfaces, arch=arch)
281         except IOError:
282             logger.log('Failed to parse or use XML template file %s'%template_filename)
283             return
284
285         # Lookup for the sliver before actually
286         # defining it, just in case it was already defined.
287         try:
288             dom = conn.lookupByName(name)
289         except:
290             dom = conn.defineXML(xml)
291         logger.verbose('lxc_create: %s -> %s'%(name, Sliver_Libvirt.dom_details(dom)))
292
293
294     @staticmethod
295     def destroy(name):
296         # umount .ssh directory - only if mounted
297         Account.umount_ssh_dir(name)
298         logger.verbose ('sliver_lxc: %s destroy'%(name))
299         conn = Sliver_Libvirt.getConnection(Sliver_LXC.TYPE)
300
301         containerDir = Sliver_LXC.CON_BASE_DIR + '/%s'%(name)
302
303         try:
304             # Destroy libvirt domain
305             dom = conn.lookupByName(name)
306         except:
307             logger.verbose('sliver_lxc.destroy: Domain %s does not exist!' % name)
308             return
309
310         # Slivers with vsys running will fail the subvolume delete
311         # removeSliverFromVsys return True if it stops vsys, telling us to start it again later
312         vsys_stopped = removeSliverFromVsys (name)
313
314         try:
315             logger.log("sliver_lxc.destroy: destroying domain %s"%name)
316             dom.destroy()
317         except:
318             logger.verbose('sliver_lxc.destroy: Domain %s not running... continuing.' % name)
319
320         try:
321             logger.log("sliver_lxc.destroy: undefining domain %s"%name)
322             dom.undefine()
323         except:
324             logger.verbose('sliver_lxc.destroy: Domain %s is not defined... continuing.' % name)
325
326         # Remove user after destroy domain to force logout
327         command = ['/usr/sbin/userdel', '-f', '-r', name]
328         logger.log_call(command)
329
330         # Remove rootfs of destroyed domain
331         command = ['btrfs', 'subvolume', 'delete', containerDir]
332         logger.log_call(command, timeout=BTRFS_TIMEOUT)
333         
334         # For some reason I am seeing this :
335         #log_call: running command btrfs subvolume delete /vservers/inri_sl1
336         #log_call: ERROR: cannot delete '/vservers/inri_sl1' - Device or resource busy
337         #log_call: Delete subvolume '/vservers/inri_sl1'
338         #log_call:end command (btrfs subvolume delete /vservers/inri_sl1) returned with code 1
339         #
340         # something must have an open handle to a file in there, but I can't find out what it is
341         # the following code aims at gathering data on what is going on in the system at this point in time
342         # note that some time later (typically when the sliver gets re-created) the same
343         # attempt at deleting the subvolume does work
344         # also lsof never shows anything relevant; this is painful..
345
346         if not os.path.exists(containerDir):
347             logger.log('sliver_lxc.destroy: %s cleanly destroyed.'%name)
348         else:
349             # we're in /
350             #logger.log("-TMP-cwd %s : %s"%(name,os.getcwd()))
351             # also lsof never shows anything relevant; this is painful..
352             #logger.log("-TMP-lsof %s"%name)
353             #command=['lsof']
354             #logger.log_call(command)
355             logger.log("-TMP-ls-l %s"%name)
356             command = ['ls', '-l', containerDir]
357             logger.log_call(command)
358             if os.path.exists(containerDir):
359                 logger.log('sliver_lxc.destroy: ERROR could not cleanly destroy %s - giving up'%name)
360
361         if vsys_stopped: vsysStartService()