1b82b45f7ea1054613f0297098b6ac2810479ac9
[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 should be deleted 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         logger.log('========== sliver_lxc.configure {}'.format(self.name))
48         Sliver_Libvirt.configure(self, rec)
49
50         # in case we update nodemanager..
51         self.install_and_enable_vinit()
52         # do the configure part from Initscript
53         Initscript.configure(self, rec)
54
55     # remember configure() always gets called *before* start()
56     # in particular the slice initscript
57     # is expected to be in place already at this point
58     def start(self, delay=0):
59         logger.log('==================== sliver_lxc.start {}'.format(self.name))
60         if 'enabled' in self.rspec and self.rspec['enabled'] <= 0:
61             logger.log('sliver_lxc: not starting {}, is not enabled'.format(self.name))
62             return
63         # the generic /etc/init.d/vinit script is permanently refreshed, and enabled
64         self.install_and_enable_vinit()
65         # expose .ssh for omf_friendly slivers
66         if 'tags' in self.rspec and 'omf_control' in self.rspec['tags']:
67             Account.mount_ssh_dir(self.name)
68 #        logger.log("NM is exiting for debug - just about to start {}".format(self.name))
69 #        exit(0)
70         Sliver_Libvirt.start(self, delay)
71
72     def rerun_slice_vinit(self):
73         """This is called at startup, and whenever the initscript code changes"""
74         logger.log("sliver_lxc.rerun_slice_vinit {}".format(self.name))
75         plain = "virsh -c lxc:/// lxc-enter-namespace --noseclabel -- {} /usr/bin/systemctl --system daemon-reload"\
76             .format(self.name)
77         command = plain.split()
78         logger.log_call(command, timeout=3)
79         plain = "virsh -c lxc:/// lxc-enter-namespace --noseclabel -- {} /usr/bin/systemctl restart vinit.service"\
80             .format(self.name)
81         command = plain.split()
82         logger.log_call(command, timeout=3)
83                 
84         
85     @staticmethod
86     def create(name, rec=None):
87         '''
88         Create dirs, copy fs image, lxc_create
89         '''
90         logger.verbose('sliver_lxc: {} create'.format(name))
91         conn = Sliver_Libvirt.getConnection(Sliver_LXC.TYPE)
92
93         vref = rec['vref']
94         if vref is None:
95             vref = "lxc-f18-x86_64"
96             logger.log("sliver_libvirt: {}: WARNING - no vref attached, using hard-wired default {}"
97                        .format(name, vref))
98
99         # compute guest arch from vref
100         # essentially we want x86_64 (default) or i686 here for libvirt
101         try:
102             (x, y, arch) = vref.split('-')
103             arch = "x86_64" if arch.find("64") >= 0 else "i686"
104         except:
105             arch = 'x86_64'
106
107         # Get the type of image from vref myplc tags specified as:
108         # pldistro = lxc
109         # fcdistro = squeeze
110         # arch x86_64
111
112         arch = 'x86_64'
113         tags = rec['rspec']['tags']
114         if 'arch' in tags:
115             arch = tags['arch']
116             if arch == 'i386':
117                 arch = 'i686'
118
119
120         refImgDir    = os.path.join(Sliver_LXC.REF_IMG_BASE_DIR, vref)
121         containerDir = os.path.join(Sliver_LXC.CON_BASE_DIR, name)
122
123         # check the template exists -- there's probably a better way..
124         if not os.path.isdir(refImgDir):
125             logger.log('sliver_lxc: {}: ERROR Could not create sliver - reference image {} not found'
126                        .format(name, vref))
127             logger.log('sliver_lxc: {}: ERROR Expected reference image in {}'.format(name, refImgDir))
128             return
129
130 # during some time this fragment had been commented out
131 # but we're seeing cases where this code might actually be useful, so..
132 #        this hopefully should be fixed now
133 #        # in fedora20 we have some difficulty in properly cleaning up /vservers/<slicename>
134 #        # also note that running e.g. btrfs subvolume create /vservers/.lvref/image /vservers/foo
135 #        # behaves differently, whether /vservers/foo exists or not:
136 #        # if /vservers/foo does not exist, it creates /vservers/foo
137 #        # but if it does exist, then       it creates /vservers/foo/image !!
138 #        # so we need to check the expected container rootfs does not exist yet
139 #        # this hopefully could be removed in a future release
140         if os.path.exists (containerDir):
141             logger.log("sliver_lxc: {}: WARNING cleaning up pre-existing {}".format(name, containerDir))
142             command = ['btrfs', 'subvolume', 'delete', containerDir]
143             logger.log_call(command, BTRFS_TIMEOUT)
144             # re-check
145             if os.path.exists (containerDir):
146                 logger.log('sliver_lxc: {}: ERROR Could not create sliver - could not clean up empty {}'
147                            .format(name, containerDir))
148                 return
149
150         # Snapshot the reference image fs
151         # this assumes the reference image is in its own subvolume
152         command = ['btrfs', 'subvolume', 'snapshot', refImgDir, containerDir]
153         if not logger.log_call(command, timeout=BTRFS_TIMEOUT):
154             logger.log('sliver_lxc: ERROR Could not create BTRFS snapshot at', containerDir)
155             return
156         command = ['chmod', '755', containerDir]
157         logger.log_call(command)
158
159         # TODO: set quotas...
160
161         # Set hostname. A valid hostname cannot have '_'
162         #with open(os.path.join(containerDir, 'etc/hostname'), 'w') as f:
163         #    print >>f, name.replace('_', '-')
164
165         # Add slices group if not already present
166         try:
167             group = grp.getgrnam('slices')
168         except:
169             command = ['/usr/sbin/groupadd', 'slices']
170             logger.log_call(command)
171
172         # Add unix account (TYPE is specified in the subclass)
173         command = ['/usr/sbin/useradd', '-g', 'slices', '-s', Sliver_LXC.SHELL, name, '-p', '*']
174         logger.log_call(command)
175         command = ['mkdir', '/home/{}/.ssh'.format(name)]
176         logger.log_call(command)
177
178         # Create PK pair keys to connect from the host to the guest without
179         # password... maybe remove the need for authentication inside the
180         # guest?
181         command = ['su', '-s', '/bin/bash', '-c',
182                    'ssh-keygen -t rsa -N "" -f /home/{}/.ssh/id_rsa'.format(name)]
183         logger.log_call(command)
184
185         command = ['chown', '-R', '{}:slices'.format(name), '/home/{}/.ssh'.format(name)]
186         logger.log_call(command)
187
188         command = ['mkdir', '{}/root/.ssh'.format(containerDir)]
189         logger.log_call(command)
190
191         command = ['cp', '/home/{}/.ssh/id_rsa.pub'.format(name),
192                    '{}/root/.ssh/authorized_keys'.format(containerDir)]
193         logger.log_call(command)
194
195         logger.log("creating /etc/slicename file in {}".format(os.path.join(containerDir, 'etc/slicename')))
196         try:
197             with open(os.path.join(containerDir, 'etc/slicename'), 'w') as f:
198                 f.write(name)
199         except:
200             logger.log_exc("exception while creating /etc/slicename")
201
202         try:
203             with open(os.path.join(containerDir, 'etc/slicefamily'), 'w') as f:
204                 f.write(vref)
205         except:
206             logger.log_exc("exception while creating /etc/slicefamily")
207
208         uid = None
209         try:
210             uid = getpwnam(name).pw_uid
211         except KeyError:
212             # keyerror will happen if user id was not created successfully
213             logger.log_exc("exception while getting user id")
214
215         if uid is not None:
216             logger.log("uid is {}".format(uid))
217             command = ['mkdir', '{}/home/{}'.format(containerDir, name)]
218             logger.log_call(command)
219             command = ['chown', name, '{}/home/{}'.format(containerDir, name)]
220             logger.log_call(command)
221             etcpasswd = os.path.join(containerDir, 'etc/passwd')
222             etcgroup = os.path.join(containerDir, 'etc/group')
223             if os.path.exists(etcpasswd):
224                 # create all accounts with gid=1001 - i.e. 'slices' like it is in the root context
225                 slices_gid = 1001
226                 logger.log("adding user {name} id {uid} gid {slices_gid} to {etcpasswd}"
227                            .format(**(locals())))
228                 try:
229                     with open(etcpasswd, 'a') as passwdfile:
230                         passwdfile.write("{name}:x:{uid}:{slices_gid}::/home/{name}:/bin/bash\n"
231                                          .format(**locals()))
232                 except:
233                     logger.log_exc("exception while updating {}".format(etcpasswd))
234                 logger.log("adding group slices with gid {slices_gid} to {etcgroup}"
235                            .format(**locals()))
236                 try:
237                     with open(etcgroup, 'a') as groupfile:
238                         groupfile.write("slices:x:{slices_gid}\n"
239                                         .format(**locals()))
240                 except:
241                     logger.log_exc("exception while updating {}".format(etcgroup))
242             sudoers = os.path.join(containerDir, 'etc/sudoers')
243             if os.path.exists(sudoers):
244                 try:
245                     with open(sudoers, 'a') as f:
246                         f.write("{} ALL=(ALL) NOPASSWD: ALL\n".format(name))
247                 except:
248                     logger.log_exc("exception while updating /etc/sudoers")
249
250         # customizations for the user environment - root or slice uid
251         # we save the whole business in /etc/planetlab.profile
252         # and source this file for both root and the slice uid's .profile
253         # prompt for slice owner, + LD_PRELOAD for transparently wrap bind
254         pl_profile = os.path.join(containerDir, "etc/planetlab.profile")
255         ld_preload_text = """# by default, we define this setting so that calls to bind(2),
256 # when invoked on 0.0.0.0, get transparently redirected to the public interface of this node
257 # see https://svn.planet-lab.org/wiki/LxcPortForwarding"""
258         usrmove_path_text = """# VM's before Features/UsrMove need /bin and /sbin in their PATH"""
259         usrmove_path_code = """
260 pathmunge () {
261         if ! echo $PATH | /bin/egrep -q "(^|:)$1($|:)" ; then
262            if [ "$2" = "after" ] ; then
263               PATH=$PATH:$1
264            else
265               PATH=$1:$PATH
266            fi
267         fi
268 }
269 pathmunge /bin after
270 pathmunge /sbin after
271 unset pathmunge
272 """
273         with open(pl_profile, 'w') as f:
274             f.write("export PS1='{}@\H \$ '\n".format(name))
275             f.write("{}\n".format(ld_preload_text))
276             f.write("export LD_PRELOAD=/etc/planetlab/lib/bind_public.so\n")
277             f.write("{}\n".format(usrmove_path_text))
278             f.write("{}\n".format(usrmove_path_code))
279
280         # make sure this file is sourced from both root's and slice's .profile
281         enforced_line = "[ -f /etc/planetlab.profile ] && source /etc/planetlab.profile\n"
282         for path in [ 'root/.profile', 'home/{}/.profile'.format(name) ]:
283             from_root = os.path.join(containerDir, path)
284             # if dir is not yet existing let's forget it for now
285             if not os.path.isdir(os.path.dirname(from_root)): continue
286             found = False
287             try:
288                 with open(from_root) as f:
289                     contents = f.readlines()
290                     for content in contents:
291                         if content == enforced_line:
292                             found = True
293             except IOError:
294                 pass
295             if not found:
296                 with open(from_root, "a") as user_profile:
297                     user_profile.write(enforced_line)
298                 # in case we create the slice's .profile when writing
299                 if from_root.find("/home") >= 0:
300                     command = ['chown', '{}:slices'.format(name), from_root]
301                     logger.log_call(command)
302
303         # Lookup for xid and create template after the user is created so we
304         # can get the correct xid based on the name of the slice
305         xid = bwlimit.get_xid(name)
306
307         # Template for libvirt sliver configuration
308         template_filename_sliceimage = os.path.join(Sliver_LXC.REF_IMG_BASE_DIR, 'lxc_template.xml')
309         if os.path.isfile(template_filename_sliceimage):
310             logger.verbose("Using XML template {}".format(template_filename_sliceimage))
311             template_filename = template_filename_sliceimage
312         else:
313             logger.log("Cannot find XML template {}".format(template_filename_sliceimage))
314             return
315
316         interfaces = Sliver_Libvirt.get_interfaces_xml(rec)
317
318         try:
319             with open(template_filename) as f:
320                 template = Template(f.read())
321                 xml  = template.substitute(name=name, xid=xid, interfaces=interfaces, arch=arch)
322         except IOError:
323             logger.log('Failed to parse or use XML template file {}'.format(template_filename))
324             return
325
326         # Lookup for the sliver before actually
327         # defining it, just in case it was already defined.
328         try:
329             dom = conn.lookupByName(name)
330         except:
331             dom = conn.defineXML(xml)
332         logger.verbose('lxc_create: {} -> {}'.format(name, Sliver_Libvirt.dom_details(dom)))
333
334
335     @staticmethod
336     def destroy(name):
337         # umount .ssh directory - only if mounted
338         Account.umount_ssh_dir(name)
339         logger.verbose ('sliver_lxc: {} destroy'.format(name))
340         conn = Sliver_Libvirt.getConnection(Sliver_LXC.TYPE)
341
342         containerDir = os.path.join(Sliver_LXC.CON_BASE_DIR, name)
343
344         try:
345             # Destroy libvirt domain
346             dom = conn.lookupByName(name)
347         except:
348             logger.verbose('sliver_lxc.destroy: Domain {} does not exist!'.format(name))
349             return
350
351         # Slivers with vsys running will fail the subvolume delete
352         # removeSliverFromVsys return True if it stops vsys, telling us to start it again later
353         vsys_stopped = removeSliverFromVsys (name)
354
355         try:
356             logger.log("sliver_lxc.destroy: destroying domain {}".format(name))
357             dom.destroy()
358         except:
359             logger.verbose("sliver_lxc.destroy: Domain {} not running... continuing.".format(name))
360
361         try:
362             logger.log("sliver_lxc.destroy: undefining domain {}".format(name))
363             dom.undefine()
364         except:
365             logger.verbose('sliver_lxc.destroy: Domain {} is not defined... continuing.'.format(name))
366
367         # Remove user after destroy domain to force logout
368         command = ['/usr/sbin/userdel', '-f', '-r', name]
369         logger.log_call(command)
370
371         # Remove rootfs of destroyed domain
372         command = ['/usr/bin/rm', '-rf', containerDir]
373         logger.log_call(command, timeout=BTRFS_TIMEOUT)
374
375         # ???
376         logger.log("-TMP-ls-l {}".format(name))
377         command = ['ls', '-lR', containerDir]
378         logger.log_call(command)
379         logger.log("-TMP-vsys-status")
380         command = ['/usr/bin/systemctl', 'status', 'vsys']
381         logger.log_call(command)
382         # ???
383
384         # Remove rootfs of destroyed domain
385         command = ['btrfs', 'subvolume', 'delete', containerDir]
386         logger.log_call(command, timeout=BTRFS_TIMEOUT)
387
388         # For some reason I am seeing this :
389         #log_call: running command btrfs subvolume delete /vservers/inri_sl1
390         #log_call: ERROR: cannot delete '/vservers/inri_sl1' - Device or resource busy
391         #log_call: Delete subvolume '/vservers/inri_sl1'
392         #log_call:end command (btrfs subvolume delete /vservers/inri_sl1) returned with code 1
393         #
394         # something must have an open handle to a file in there, but I can't find out what it is
395         # the following code aims at gathering data on what is going on in the system at this point in time
396         # note that some time later (typically when the sliver gets re-created) the same
397         # attempt at deleting the subvolume does work
398         # also lsof never shows anything relevant; this is painful..
399
400         if not os.path.exists(containerDir):
401             logger.log('sliver_lxc.destroy: {} cleanly destroyed.'.format(name))
402         else:
403             # we're in /
404             #logger.log("-TMP-cwd {} : {}".format(name, os.getcwd()))
405             # also lsof never shows anything relevant; this is painful..
406             #logger.log("-TMP-lsof {}".format(name))
407             #command = ['lsof']
408             #logger.log_call(command)
409             logger.log("-TMP-ls-l {}".format(name))
410             command = ['ls', '-lR', containerDir]
411             logger.log_call(command)
412             logger.log("-TMP-lsof")
413             command = ['lsof']
414             logger.log_call(command)
415             if os.path.exists(containerDir):
416                 logger.log('sliver_lxc.destroy: ERROR could not cleanly destroy {} - giving up'.format(name))
417
418         if vsys_stopped:
419             vsysStartService()