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