missing import
[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             logger.log('sliver_libvirt: %s: WARNING - no vref attached defaults to lxc-f14' % (name))
87             vref = "lxc-f14-x86_64"
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         # customizations for the user environment - root or slice uid
108         # we save the whole business in /etc/planetlab.profile 
109         # and source this file for both root and the slice uid's .profile
110         # prompt for slice owner, + LD_PRELOAD for transparently wrap bind
111         pl_profile=os.path.join(containerDir,"etc/planetlab.profile")
112         ld_preload_msg="""# by default, we define this setting so that calls to bind(2),
113 # when invoked on 0.0.0.0, get transparently redirected to the public interface of this node
114 # see https://svn.planet-lab.org/wiki/LxcPortForwarding"""
115         usrmove_path_msg="""# VM's before Features/UsrMove need /bin and /sbin in their PATH"""
116         usrmove_path_code="""
117 pathmunge () {
118         if ! echo $PATH | /bin/egrep -q "(^|:)$1($|:)" ; then
119            if [ "$2" = "after" ] ; then
120               PATH=$PATH:$1
121            else
122               PATH=$1:$PATH
123            fi
124         fi
125 }
126 pathmunge /bin after
127 pathmunge /sbin after
128 unset pathmunge
129 """
130         with open(dot_profile,'w') as f:
131             f.write("export PS1='%s@\H \$ '\n"%(name))
132             f.write("%s\n"%ld_preload_msg)
133             f.write("export LD_PRELOAD=/etc/planetlab/lib/bind_public.so\n")
134             f.write("%s\n"%usrmove_path_msg)
135             f.write("%s\n"%usrmove_path_code)
136
137         # make sure this file is sourced from both root's and slice's .profile
138         enforced_line = "[ -f /etc/planetlab.profile ] && source /etc/planetlab.profile\n"
139         for path in [ 'root/.profile', 'home/%s/.profile'%name ]:
140             from_root=os.path,join(containerDir,path)
141             found=False
142             try: 
143                 contents=file(from_root).readlnes()
144                 for content in contents:
145                     if content==enforced_line: found=True
146             except:
147                 pass
148             if not found:
149                 with open(from_root,"a") as user_profile:
150                     user_profile.write(enforced_line)
151
152         # TODO: set quotas...
153
154         # Set hostname. A valid hostname cannot have '_'
155         #with open(os.path.join(containerDir, 'etc/hostname'), 'w') as f:
156         #    print >>f, name.replace('_', '-')
157
158         # Add slices group if not already present
159         try:
160             group = grp.getgrnam('slices')
161         except:
162             command = ['/usr/sbin/groupadd', 'slices']
163             logger.log_call(command, timeout=15*60)
164
165         # Add unix account (TYPE is specified in the subclass)
166         command = ['/usr/sbin/useradd', '-g', 'slices', '-s', Sliver_LXC.SHELL, name, '-p', '*']
167         logger.log_call(command, timeout=15*60)
168         command = ['mkdir', '/home/%s/.ssh'%name]
169         logger.log_call(command, timeout=15*60)
170
171         # Create PK pair keys to connect from the host to the guest without
172         # password... maybe remove the need for authentication inside the
173         # guest?
174         command = ['su', '-s', '/bin/bash', '-c', 'ssh-keygen -t rsa -N "" -f /home/%s/.ssh/id_rsa'%(name)]
175         logger.log_call(command, timeout=60)
176
177         command = ['chown', '-R', '%s.slices'%name, '/home/%s/.ssh'%name]
178         logger.log_call(command, timeout=30)
179
180         command = ['mkdir', '%s/root/.ssh'%containerDir]
181         logger.log_call(command, timeout=10)
182
183         command = ['cp', '/home/%s/.ssh/id_rsa.pub'%name, '%s/root/.ssh/authorized_keys'%containerDir]
184         logger.log_call(command, timeout=30)
185
186         logger.log("creating /etc/slicename file in %s" % os.path.join(containerDir,'etc/slicename'))
187         try:
188             file(os.path.join(containerDir,'etc/slicename'), 'w').write(name)
189         except:
190             logger.log_exc("exception while creating /etc/slicename")
191
192         try:
193             file(os.path.join(containerDir,'etc/slicefamily'), 'w').write(vref)
194         except:
195             logger.log_exc("exception while creating /etc/slicefamily")
196
197         uid = None
198         try:
199             uid = getpwnam(name).pw_uid
200         except KeyError:
201             # keyerror will happen if user id was not created successfully
202             logger.log_exc("exception while getting user id")
203
204         if uid is not None:
205             logger.log("uid is %d" % uid)
206             command = ['mkdir', '%s/home/%s' % (containerDir, name)]
207             logger.log_call(command, timeout=10)
208             command = ['chown', name, '%s/home/%s' % (containerDir, name)]
209             logger.log_call(command, timeout=10)
210             etcpasswd = os.path.join(containerDir, 'etc/passwd')
211             if os.path.exists(etcpasswd):
212                 logger.log("adding user %s id %d to %s" % (name, uid, etcpasswd))
213                 try:
214                     file(etcpasswd,'a').write("%s:x:%d:%d::/home/%s:/bin/bash\n" % (name, uid, uid, name))
215                 except:
216                     logger.log_exc("exception while updating etc/passwd")
217             sudoers = os.path.join(containerDir, 'etc/sudoers')
218             if os.path.exists(sudoers):
219                 try:
220                     file(sudoers,'a').write("%s ALL=(ALL) NOPASSWD: ALL\n" % name)
221                 except:
222                     logger.log_exc("exception while updating /etc/sudoers")
223
224         # Lookup for xid and create template after the user is created so we
225         # can get the correct xid based on the name of the slice
226         xid = bwlimit.get_xid(name)
227
228         # Template for libvirt sliver configuration
229         template_filename_sliceimage = os.path.join(Sliver_LXC.REF_IMG_BASE_DIR,'lxc_template.xml')
230         if os.path.isfile (template_filename_sliceimage):
231             logger.log("WARNING: using compat template %s"%template_filename_sliceimage)
232             template_filename=template_filename_sliceimage
233         else:
234             logger.log("Cannot find XML template %s"%template_filename_sliceimage)
235             return
236
237         interfaces = Sliver_Libvirt.get_interfaces_xml(rec)
238
239         try:
240             with open(template_filename) as f:
241                 template = Template(f.read())
242                 xml  = template.substitute(name=name, xid=xid, interfaces=interfaces, arch=arch)
243         except IOError:
244             logger.log('Failed to parse or use XML template file %s'%template_filename)
245             return
246
247         # Lookup for the sliver before actually
248         # defining it, just in case it was already defined.
249         try:
250             dom = conn.lookupByName(name)
251         except:
252             dom = conn.defineXML(xml)
253         logger.verbose('lxc_create: %s -> %s'%(name, Sliver_Libvirt.debuginfo(dom)))
254
255
256     @staticmethod
257     def destroy(name):
258         # umount .ssh directory - only if mounted
259         Account.umount_ssh_dir(name)
260         logger.verbose ('sliver_lxc: %s destroy'%(name))
261         conn = Sliver_Libvirt.getConnection(Sliver_LXC.TYPE)
262
263         containerDir = Sliver_LXC.CON_BASE_DIR + '/%s'%(name)
264
265         try:
266             # Destroy libvirt domain
267             dom = conn.lookupByName(name)
268         except:
269             logger.verbose('sliver_lxc: Domain %s does not exist!' % name)
270
271         try:
272             dom.destroy()
273         except:
274             logger.verbose('sliver_lxc: Domain %s not running... continuing.' % name)
275
276         try:
277             dom.undefine()
278         except:
279             logger.verbose('sliver_lxc: Domain %s is not defined... continuing.' % name)
280
281         # Remove user after destroy domain to force logout
282         command = ['/usr/sbin/userdel', '-f', '-r', name]
283         logger.log_call(command, timeout=15*60)
284
285         if os.path.exists(os.path.join(containerDir,"vsys")):
286             # Slivers with vsys running will fail the subvolume delete.
287             # A more permanent solution may be to ensure that the vsys module
288             # is called before the sliver is destroyed.
289             logger.log("destroying vsys directory and restarting vsys")
290             logger.log_call(["rm", "-fR", os.path.join(containerDir, "vsys")])
291             logger.log_call(["/etc/init.d/vsys", "restart", ])
292
293         # Remove rootfs of destroyed domain
294         command = ['btrfs', 'subvolume', 'delete', containerDir]
295         logger.log_call(command, timeout=60)
296
297         if os.path.exists(containerDir):
298            # oh no, it's still here...
299            logger.log("WARNING: failed to destroy container %s" % containerDir)
300
301         logger.verbose('sliver_libvirt: %s destroyed.'%name)
302