make sure the uid entry in a slice has a gid that solves
[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         # 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
233         # Lookup for xid and create template after the user is created so we
234         # can get the correct xid based on the name of the slice
235         xid = bwlimit.get_xid(name)
236
237         # Template for libvirt sliver configuration
238         template_filename_sliceimage = os.path.join(Sliver_LXC.REF_IMG_BASE_DIR,'lxc_template.xml')
239         if os.path.isfile (template_filename_sliceimage):
240             logger.log("WARNING: using compat template %s"%template_filename_sliceimage)
241             template_filename=template_filename_sliceimage
242         else:
243             logger.log("Cannot find XML template %s"%template_filename_sliceimage)
244             return
245
246         interfaces = Sliver_Libvirt.get_interfaces_xml(rec)
247
248         try:
249             with open(template_filename) as f:
250                 template = Template(f.read())
251                 xml  = template.substitute(name=name, xid=xid, interfaces=interfaces, arch=arch)
252         except IOError:
253             logger.log('Failed to parse or use XML template file %s'%template_filename)
254             return
255
256         # Lookup for the sliver before actually
257         # defining it, just in case it was already defined.
258         try:
259             dom = conn.lookupByName(name)
260         except:
261             dom = conn.defineXML(xml)
262         logger.verbose('lxc_create: %s -> %s'%(name, Sliver_Libvirt.debuginfo(dom)))
263
264
265     @staticmethod
266     def destroy(name):
267         # umount .ssh directory - only if mounted
268         Account.umount_ssh_dir(name)
269         logger.verbose ('sliver_lxc: %s destroy'%(name))
270         conn = Sliver_Libvirt.getConnection(Sliver_LXC.TYPE)
271
272         containerDir = Sliver_LXC.CON_BASE_DIR + '/%s'%(name)
273
274         try:
275             # Destroy libvirt domain
276             dom = conn.lookupByName(name)
277         except:
278             logger.verbose('sliver_lxc: Domain %s does not exist!' % name)
279
280         try:
281             dom.destroy()
282         except:
283             logger.verbose('sliver_lxc: Domain %s not running... continuing.' % name)
284
285         try:
286             dom.undefine()
287         except:
288             logger.verbose('sliver_lxc: Domain %s is not defined... continuing.' % name)
289
290         # Remove user after destroy domain to force logout
291         command = ['/usr/sbin/userdel', '-f', '-r', name]
292         logger.log_call(command, timeout=15*60)
293
294         if os.path.exists(os.path.join(containerDir,"vsys")):
295             # Slivers with vsys running will fail the subvolume delete.
296             # A more permanent solution may be to ensure that the vsys module
297             # is called before the sliver is destroyed.
298             logger.log("destroying vsys directory and restarting vsys")
299             logger.log_call(["rm", "-fR", os.path.join(containerDir, "vsys")])
300             logger.log_call(["/etc/init.d/vsys", "restart", ])
301
302         # Remove rootfs of destroyed domain
303         command = ['btrfs', 'subvolume', 'delete', containerDir]
304         logger.log_call(command, timeout=60)
305
306         if os.path.exists(containerDir):
307            # oh no, it's still here...
308            logger.log("WARNING: failed to destroy container %s" % containerDir)
309
310         logger.verbose('sliver_libvirt: %s destroyed.'%name)
311