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