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