cosmetic
[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-f14-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         # 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(pl_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             # if dir is not yet existing let's forget it for now
142             if not os.path.isdir(os.path.dirname(from_root)): continue
143             found=False
144             try: 
145                 contents=file(from_root).readlines()
146                 for content in contents:
147                     if content==enforced_line: found=True
148             except IOError: pass
149             if not found:
150                 with open(from_root,"a") as user_profile:
151                     user_profile.write(enforced_line)
152
153         # TODO: set quotas...
154
155         # Set hostname. A valid hostname cannot have '_'
156         #with open(os.path.join(containerDir, 'etc/hostname'), 'w') as f:
157         #    print >>f, name.replace('_', '-')
158
159         # Add slices group if not already present
160         try:
161             group = grp.getgrnam('slices')
162         except:
163             command = ['/usr/sbin/groupadd', 'slices']
164             logger.log_call(command, timeout=15*60)
165
166         # Add unix account (TYPE is specified in the subclass)
167         command = ['/usr/sbin/useradd', '-g', 'slices', '-s', Sliver_LXC.SHELL, name, '-p', '*']
168         logger.log_call(command, timeout=15*60)
169         command = ['mkdir', '/home/%s/.ssh'%name]
170         logger.log_call(command, timeout=15*60)
171
172         # Create PK pair keys to connect from the host to the guest without
173         # password... maybe remove the need for authentication inside the
174         # guest?
175         command = ['su', '-s', '/bin/bash', '-c', 'ssh-keygen -t rsa -N "" -f /home/%s/.ssh/id_rsa'%(name)]
176         logger.log_call(command, timeout=60)
177
178         command = ['chown', '-R', '%s.slices'%name, '/home/%s/.ssh'%name]
179         logger.log_call(command, timeout=30)
180
181         command = ['mkdir', '%s/root/.ssh'%containerDir]
182         logger.log_call(command, timeout=10)
183
184         command = ['cp', '/home/%s/.ssh/id_rsa.pub'%name, '%s/root/.ssh/authorized_keys'%containerDir]
185         logger.log_call(command, timeout=30)
186
187         logger.log("creating /etc/slicename file in %s" % os.path.join(containerDir,'etc/slicename'))
188         try:
189             file(os.path.join(containerDir,'etc/slicename'), 'w').write(name)
190         except:
191             logger.log_exc("exception while creating /etc/slicename")
192
193         try:
194             file(os.path.join(containerDir,'etc/slicefamily'), 'w').write(vref)
195         except:
196             logger.log_exc("exception while creating /etc/slicefamily")
197
198         uid = None
199         try:
200             uid = getpwnam(name).pw_uid
201         except KeyError:
202             # keyerror will happen if user id was not created successfully
203             logger.log_exc("exception while getting user id")
204
205         if uid is not None:
206             logger.log("uid is %d" % uid)
207             command = ['mkdir', '%s/home/%s' % (containerDir, name)]
208             logger.log_call(command, timeout=10)
209             command = ['chown', name, '%s/home/%s' % (containerDir, name)]
210             logger.log_call(command, timeout=10)
211             etcpasswd = os.path.join(containerDir, 'etc/passwd')
212             if os.path.exists(etcpasswd):
213                 logger.log("adding user %s id %d to %s" % (name, uid, etcpasswd))
214                 try:
215                     file(etcpasswd,'a').write("%s:x:%d:%d::/home/%s:/bin/bash\n" % (name, uid, uid, name))
216                 except:
217                     logger.log_exc("exception while updating etc/passwd")
218             sudoers = os.path.join(containerDir, 'etc/sudoers')
219             if os.path.exists(sudoers):
220                 try:
221                     file(sudoers,'a').write("%s ALL=(ALL) NOPASSWD: ALL\n" % name)
222                 except:
223                     logger.log_exc("exception while updating /etc/sudoers")
224
225         # Lookup for xid and create template after the user is created so we
226         # can get the correct xid based on the name of the slice
227         xid = bwlimit.get_xid(name)
228
229         # Template for libvirt sliver configuration
230         template_filename_sliceimage = os.path.join(Sliver_LXC.REF_IMG_BASE_DIR,'lxc_template.xml')
231         if os.path.isfile (template_filename_sliceimage):
232             logger.log("WARNING: using compat template %s"%template_filename_sliceimage)
233             template_filename=template_filename_sliceimage
234         else:
235             logger.log("Cannot find XML template %s"%template_filename_sliceimage)
236             return
237
238         interfaces = Sliver_Libvirt.get_interfaces_xml(rec)
239
240         try:
241             with open(template_filename) as f:
242                 template = Template(f.read())
243                 xml  = template.substitute(name=name, xid=xid, interfaces=interfaces, arch=arch)
244         except IOError:
245             logger.log('Failed to parse or use XML template file %s'%template_filename)
246             return
247
248         # Lookup for the sliver before actually
249         # defining it, just in case it was already defined.
250         try:
251             dom = conn.lookupByName(name)
252         except:
253             dom = conn.defineXML(xml)
254         logger.verbose('lxc_create: %s -> %s'%(name, Sliver_Libvirt.debuginfo(dom)))
255
256
257     @staticmethod
258     def destroy(name):
259         # umount .ssh directory - only if mounted
260         Account.umount_ssh_dir(name)
261         logger.verbose ('sliver_lxc: %s destroy'%(name))
262         conn = Sliver_Libvirt.getConnection(Sliver_LXC.TYPE)
263
264         containerDir = Sliver_LXC.CON_BASE_DIR + '/%s'%(name)
265
266         try:
267             # Destroy libvirt domain
268             dom = conn.lookupByName(name)
269         except:
270             logger.verbose('sliver_lxc: Domain %s does not exist!' % name)
271
272         try:
273             dom.destroy()
274         except:
275             logger.verbose('sliver_lxc: Domain %s not running... continuing.' % name)
276
277         try:
278             dom.undefine()
279         except:
280             logger.verbose('sliver_lxc: Domain %s is not defined... continuing.' % name)
281
282         # Remove user after destroy domain to force logout
283         command = ['/usr/sbin/userdel', '-f', '-r', name]
284         logger.log_call(command, timeout=15*60)
285
286         if os.path.exists(os.path.join(containerDir,"vsys")):
287             # Slivers with vsys running will fail the subvolume delete.
288             # A more permanent solution may be to ensure that the vsys module
289             # is called before the sliver is destroyed.
290             logger.log("destroying vsys directory and restarting vsys")
291             logger.log_call(["rm", "-fR", os.path.join(containerDir, "vsys")])
292             logger.log_call(["/etc/init.d/vsys", "restart", ])
293
294         # Remove rootfs of destroyed domain
295         command = ['btrfs', 'subvolume', 'delete', containerDir]
296         logger.log_call(command, timeout=60)
297
298         if os.path.exists(containerDir):
299            # oh no, it's still here...
300            logger.log("WARNING: failed to destroy container %s" % containerDir)
301
302         logger.verbose('sliver_libvirt: %s destroyed.'%name)
303