Add xid to template match
[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 import libvirt
14
15 import logger
16 import plnode.bwlimit as bwlimit
17 from initscript import Initscript
18 from sliver_libvirt import Sliver_Libvirt
19
20 class Sliver_LXC(Sliver_Libvirt, Initscript):
21     """This class wraps LXC commands"""
22
23     SHELL = '/usr/sbin/vsh'
24     TYPE = 'sliver.LXC'
25     # Need to add a tag at myplc to actually use this account
26     # type = 'sliver.LXC'
27
28     REF_IMG_BASE_DIR = '/vservers/.lvref'
29     CON_BASE_DIR     = '/vservers'
30
31     def __init__ (self, rec):
32         name=rec['name']
33         Sliver_Libvirt.__init__ (self,rec)
34         Initscript.__init__ (self,name)
35
36     def configure (self, rec):
37         Sliver_Libvirt.configure (self,rec)
38
39         # in case we update nodemanager..
40         self.install_and_enable_vinit()
41         # do the configure part from Initscript
42         Initscript.configure(self,rec)
43
44     def start(self, delay=0):
45         if 'enabled' in self.rspec and self.rspec['enabled'] <= 0:
46             logger.log('sliver_lxc: not starting %s, is not enabled'%self.name)
47             return
48         # the generic /etc/init.d/vinit script is permanently refreshed, and enabled
49         self.install_and_enable_vinit()
50         # expose .ssh for omf_friendly slivers
51         if 'omf_control' in self.rspec['tags']:
52             Account.mount_ssh_dir(self.name)
53         Sliver_Libvirt.start (self, delay)
54         # if a change has occured in the slice initscript, reflect this in /etc/init.d/vinit.slice
55         self.refresh_slice_vinit()
56
57     def rerun_slice_vinit (self):
58         """This is called whenever the initscript code changes"""
59         # xxx - todo - not sure exactly how to:
60         # (.) invoke something in the guest
61         # (.) which options of systemctl should be used to trigger a restart
62         # should not prevent the first run from going fine hopefully
63         logger.log("WARNING: sliver_lxc.rerun_slice_vinit not implemented yet")
64
65     @staticmethod
66     def create(name, rec=None):
67         ''' Create dirs, copy fs image, lxc_create '''
68         logger.verbose ('sliver_lxc: %s create'%(name))
69         conn = Sliver_Libvirt.getConnection(Sliver_LXC.TYPE)
70
71         # Get the type of image from vref myplc tags specified as:
72         # pldistro = lxc
73         # fcdistro = squeeze
74         # arch x86_64
75
76         arch = 'x86_64'
77         tags = rec['rspec']['tags']
78         if 'arch' in tags:
79             arch = tags['arch']
80             if arch == 'i386':
81                 arch = 'i686'
82
83         vref = rec['vref']
84         if vref is None:
85             logger.log('sliver_libvirt: %s: WARNING - no vref attached defaults to lxc-f14' % (name))
86             vref = "lxc-f14-x86_64"
87
88         refImgDir    = os.path.join(Sliver_LXC.REF_IMG_BASE_DIR, vref)
89         containerDir = os.path.join(Sliver_LXC.CON_BASE_DIR, name)
90
91         # check the template exists -- there's probably a better way..
92         if not os.path.isdir(refImgDir):
93             logger.log('sliver_lxc: %s: ERROR Could not create sliver - reference image %s not found' % (name,vref))
94             logger.log('sliver_lxc: %s: ERROR Expected reference image in %s'%(name,refImgDir))
95             return
96
97         # Snapshot the reference image fs (assume the reference image is in its own
98         # subvolume)
99         command = ['btrfs', 'subvolume', 'snapshot', refImgDir, containerDir]
100         if not logger.log_call(command, timeout=15*60):
101             logger.log('sliver_lxc: ERROR Could not create BTRFS snapshot at', containerDir)
102             return
103         command = ['chmod', '755', containerDir]
104         logger.log_call(command, timeout=15*60)
105
106         # customize prompt for slice owner, + LD_PRELOAD for transparently wrap bind
107         dot_profile=os.path.join(containerDir,"root/.profile")
108         ld_preload_msg="""# by default, we define this setting so that calls to bind(2),
109 # when invoked on 0.0.0.0, get transparently redirected to the public interface of this node
110 # see https://svn.planet-lab.org/wiki/LxcPortForwarding"""
111         usrmove_path_msg="""# VM's before Features/UsrMove need /bin and /sbin in their PATH"""
112         usrmove_path_code="""
113 pathmunge () {
114         if ! echo $PATH | /bin/egrep -q "(^|:)$1($|:)" ; then
115            if [ "$2" = "after" ] ; then
116               PATH=$PATH:$1
117            else
118               PATH=$1:$PATH
119            fi
120         fi
121 }
122 pathmunge /bin after
123 pathmunge /sbin after
124 unset pathmunge
125 """
126         with open(dot_profile,'w') as f:
127             f.write("export PS1='%s@\H \$ '\n"%(name))
128             f.write("%s\n"%ld_preload_msg)
129             f.write("export LD_PRELOAD=/etc/planetlab/lib/bind_public.so\n")
130             f.write("%s\n"%usrmove_path_msg)
131             f.write("%s\n"%usrmove_path_code)
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, timeout=15*60)
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, timeout=15*60)
149         command = ['mkdir', '/home/%s/.ssh'%name]
150         logger.log_call(command, timeout=15*60)
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, timeout=60)
157
158         command = ['chown', '-R', '%s.slices'%name, '/home/%s/.ssh'%name]
159         logger.log_call(command, timeout=30)
160
161         command = ['mkdir', '%s/root/.ssh'%containerDir]
162         logger.log_call(command, timeout=10)
163
164         command = ['cp', '/home/%s/.ssh/id_rsa.pub'%name, '%s/root/.ssh/authorized_keys'%containerDir]
165         logger.log_call(command, timeout=30)
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, timeout=10)
189             command = ['chown', name, '%s/home/%s' % (containerDir, name)]
190             logger.log_call(command, timeout=10)
191             etcpasswd = os.path.join(containerDir, 'etc/passwd')
192             if os.path.exists(etcpasswd):
193                 logger.log("adding user %s id %d to %s" % (name, uid, etcpasswd))
194                 try:
195                     file(etcpasswd,'a').write("%s:x:%d:%d::/home/%s:/bin/bash\n" % (name, uid, uid, name))
196                 except:
197                     logger.log_exc("exception while updating etc/passwd")
198             sudoers = os.path.join(containerDir, 'etc/sudoers')
199             if os.path.exists(sudoers):
200                 try:
201                     file(sudoers,'a').write("%s ALL=(ALL) NOPASSWD: ALL\n" % name)
202                 except:
203                     logger.log_exc("exception while updating /etc/sudoers")
204
205         # Lookup for xid and create template after the user is created so we
206         # can get the correct xid based on the name of the slice
207         xid = bwlimit.get_xid(name)
208
209         # Template for libvirt sliver configuration
210         template_filename_sliceimage = os.path.join(Sliver_LXC.REF_IMG_BASE_DIR,'lxc_template.xml')
211         if os.path.isfile (template_filename_sliceimage):
212             logger.log("WARNING: using compat template %s"%template_filename_sliceimage)
213             template_filename=template_filename_sliceimage
214         else:
215             logger.log("Cannot find XML template %s"%template_filename_sliceimage)
216             return
217
218         interfaces = Sliver_Libvirt.get_interfaces_xml(rec)
219
220         try:
221             with open(template_filename) as f:
222                 template = Template(f.read())
223                 xml  = template.substitute(name=name, xid=xid, interfaces=interfaces, arch=arch)
224         except IOError:
225             logger.log('Failed to parse or use XML template file %s'%template_filename)
226             return
227
228         # Lookup for the sliver before actually
229         # defining it, just in case it was already defined.
230         try:
231             dom = conn.lookupByName(name)
232         except:
233             dom = conn.defineXML(xml)
234         logger.verbose('lxc_create: %s -> %s'%(name, Sliver_Libvirt.debuginfo(dom)))
235
236
237     @staticmethod
238     def destroy(name):
239         # umount .ssh directory - only if mounted
240         Account.umount_ssh_dir(name)
241         logger.verbose ('sliver_lxc: %s destroy'%(name))
242         conn = Sliver_Libvirt.getConnection(Sliver_LXC.TYPE)
243
244         containerDir = Sliver_LXC.CON_BASE_DIR + '/%s'%(name)
245
246         try:
247             # Destroy libvirt domain
248             dom = conn.lookupByName(name)
249         except:
250             logger.verbose('sliver_lxc: Domain %s does not exist!' % name)
251
252         try:
253             dom.destroy()
254         except:
255             logger.verbose('sliver_lxc: Domain %s not running... continuing.' % name)
256
257         try:
258             dom.undefine()
259         except:
260             logger.verbose('sliver_lxc: Domain %s is not defined... continuing.' % name)
261
262         # Remove user after destroy domain to force logout
263         command = ['/usr/sbin/userdel', '-f', '-r', name]
264         logger.log_call(command, timeout=15*60)
265
266         if os.path.exists(os.path.join(containerDir,"vsys")):
267             # Slivers with vsys running will fail the subvolume delete.
268             # A more permanent solution may be to ensure that the vsys module
269             # is called before the sliver is destroyed.
270             logger.log("destroying vsys directory and restarting vsys")
271             logger.log_call(["rm", "-fR", os.path.join(containerDir, "vsys")])
272             logger.log_call(["/etc/init.d/vsys", "restart", ])
273
274         # Remove rootfs of destroyed domain
275         command = ['btrfs', 'subvolume', 'delete', containerDir]
276         logger.log_call(command, timeout=60)
277
278         if os.path.exists(containerDir):
279            # oh no, it's still here...
280            logger.log("WARNING: failed to destroy container %s" % containerDir)
281
282         logger.verbose('sliver_libvirt: %s destroyed.'%name)
283