blob: 66644ccb6a6a09120336ab83319f783514e85c54 [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001# Development tool - deploy/undeploy command plugin
2#
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05003# Copyright (C) 2014-2016 Intel Corporation
Patrick Williamsc124f4f2015-09-15 14:41:29 -05004#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 2 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program; if not, write to the Free Software Foundation, Inc.,
16# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17"""Devtool plugin containing the deploy subcommands"""
18
19import os
20import subprocess
21import logging
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050022import tempfile
23import shutil
24import argparse_oe
Patrick Williamsf1e5d692016-03-30 15:21:19 -050025from devtool import exec_fakeroot, setup_tinfoil, check_workspace_recipe, DevtoolError
Patrick Williamsc124f4f2015-09-15 14:41:29 -050026
27logger = logging.getLogger('devtool')
28
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050029deploylist_path = '/.devtool'
30
31def _prepare_remote_script(deploy, verbose=False, dryrun=False, undeployall=False, nopreserve=False, nocheckspace=False):
32 """
33 Prepare a shell script for running on the target to
34 deploy/undeploy files. We have to be careful what we put in this
35 script - only commands that are likely to be available on the
36 target are suitable (the target might be constrained, e.g. using
37 busybox rather than bash with coreutils).
38 """
39 lines = []
40 lines.append('#!/bin/sh')
41 lines.append('set -e')
42 if undeployall:
43 # Yes, I know this is crude - but it does work
44 lines.append('for entry in %s/*.list; do' % deploylist_path)
45 lines.append('[ ! -f $entry ] && exit')
46 lines.append('set `basename $entry | sed "s/.list//"`')
47 if dryrun:
48 if not deploy:
49 lines.append('echo "Previously deployed files for $1:"')
50 lines.append('manifest="%s/$1.list"' % deploylist_path)
51 lines.append('preservedir="%s/$1.preserve"' % deploylist_path)
52 lines.append('if [ -f $manifest ] ; then')
53 # Read manifest in reverse and delete files / remove empty dirs
54 lines.append(' sed \'1!G;h;$!d\' $manifest | while read file')
55 lines.append(' do')
56 if dryrun:
57 lines.append(' if [ ! -d $file ] ; then')
58 lines.append(' echo $file')
59 lines.append(' fi')
60 else:
61 lines.append(' if [ -d $file ] ; then')
62 # Avoid deleting a preserved directory in case it has special perms
63 lines.append(' if [ ! -d $preservedir/$file ] ; then')
64 lines.append(' rmdir $file > /dev/null 2>&1 || true')
65 lines.append(' fi')
66 lines.append(' else')
67 lines.append(' rm $file')
68 lines.append(' fi')
69 lines.append(' done')
70 if not dryrun:
71 lines.append(' rm $manifest')
72 if not deploy and not dryrun:
73 # May as well remove all traces
74 lines.append(' rmdir `dirname $manifest` > /dev/null 2>&1 || true')
75 lines.append('fi')
76
77 if deploy:
78 if not nocheckspace:
79 # Check for available space
80 # FIXME This doesn't take into account files spread across multiple
81 # partitions, but doing that is non-trivial
82 # Find the part of the destination path that exists
83 lines.append('checkpath="$2"')
84 lines.append('while [ "$checkpath" != "/" ] && [ ! -e $checkpath ]')
85 lines.append('do')
86 lines.append(' checkpath=`dirname "$checkpath"`')
87 lines.append('done')
88 lines.append('freespace=`df -P $checkpath | sed "1d" | awk \'{ print $4 }\'`')
89 # First line of the file is the total space
90 lines.append('total=`head -n1 $3`')
91 lines.append('if [ $total -gt $freespace ] ; then')
92 lines.append(' echo "ERROR: insufficient space on target (available ${freespace}, needed ${total})"')
93 lines.append(' exit 1')
94 lines.append('fi')
95 if not nopreserve:
96 # Preserve any files that exist. Note that this will add to the
97 # preserved list with successive deployments if the list of files
98 # deployed changes, but because we've deleted any previously
99 # deployed files at this point it will never preserve anything
100 # that was deployed, only files that existed prior to any deploying
101 # (which makes the most sense)
102 lines.append('cat $3 | sed "1d" | while read file fsize')
103 lines.append('do')
104 lines.append(' if [ -e $file ] ; then')
105 lines.append(' dest="$preservedir/$file"')
106 lines.append(' mkdir -p `dirname $dest`')
107 lines.append(' mv $file $dest')
108 lines.append(' fi')
109 lines.append('done')
110 lines.append('rm $3')
111 lines.append('mkdir -p `dirname $manifest`')
112 lines.append('mkdir -p $2')
113 if verbose:
114 lines.append(' tar xv -C $2 -f - | tee $manifest')
115 else:
116 lines.append(' tar xv -C $2 -f - > $manifest')
117 lines.append('sed -i "s!^./!$2!" $manifest')
118 elif not dryrun:
119 # Put any preserved files back
120 lines.append('if [ -d $preservedir ] ; then')
121 lines.append(' cd $preservedir')
122 lines.append(' find . -type f -exec mv {} /{} \;')
123 lines.append(' cd /')
124 lines.append(' rm -rf $preservedir')
125 lines.append('fi')
126
127 if undeployall:
128 if not dryrun:
129 lines.append('echo "NOTE: Successfully undeployed $1"')
130 lines.append('done')
131
132 # Delete the script itself
133 lines.append('rm $0')
134 lines.append('')
135
136 return '\n'.join(lines)
137
138
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500139def deploy(args, config, basepath, workspace):
140 """Entry point for the devtool 'deploy' subcommand"""
141 import re
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500142 import math
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500143 import oe.recipeutils
144
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500145 check_workspace_recipe(workspace, args.recipename, checksrc=False)
146
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500147 try:
148 host, destdir = args.target.split(':')
149 except ValueError:
150 destdir = '/'
151 else:
152 args.target = host
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500153 if not destdir.endswith('/'):
154 destdir += '/'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500155
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500156 tinfoil = setup_tinfoil(basepath=basepath)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500157 try:
158 rd = oe.recipeutils.parse_recipe_simple(tinfoil.cooker, args.recipename, tinfoil.config_data)
159 except Exception as e:
160 raise DevtoolError('Exception parsing recipe %s: %s' %
161 (args.recipename, e))
162 recipe_outdir = rd.getVar('D', True)
163 if not os.path.exists(recipe_outdir) or not os.listdir(recipe_outdir):
164 raise DevtoolError('No files to deploy - have you built the %s '
165 'recipe? If so, the install step has not installed '
166 'any files.' % args.recipename)
167
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500168 filelist = []
169 ftotalsize = 0
170 for root, _, files in os.walk(recipe_outdir):
171 for fn in files:
172 # Get the size in kiB (since we'll be comparing it to the output of du -k)
173 # MUST use lstat() here not stat() or getfilesize() since we don't want to
174 # dereference symlinks
175 fsize = int(math.ceil(float(os.lstat(os.path.join(root, fn)).st_size)/1024))
176 ftotalsize += fsize
177 # The path as it would appear on the target
178 fpath = os.path.join(destdir, os.path.relpath(root, recipe_outdir), fn)
179 filelist.append((fpath, fsize))
180
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500181 if args.dry_run:
182 print('Files to be deployed for %s on target %s:' % (args.recipename, args.target))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500183 for item, _ in filelist:
184 print(' %s' % item)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500185 return 0
186
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500187
188 extraoptions = ''
189 if args.no_host_check:
190 extraoptions += '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
191 if not args.show_status:
192 extraoptions += ' -q'
193
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500194 # In order to delete previously deployed files and have the manifest file on
195 # the target, we write out a shell script and then copy it to the target
196 # so we can then run it (piping tar output to it).
197 # (We cannot use scp here, because it doesn't preserve symlinks.)
198 tmpdir = tempfile.mkdtemp(prefix='devtool')
199 try:
200 tmpscript = '/tmp/devtool_deploy.sh'
201 tmpfilelist = os.path.join(os.path.dirname(tmpscript), 'devtool_deploy.list')
202 shellscript = _prepare_remote_script(deploy=True,
203 verbose=args.show_status,
204 nopreserve=args.no_preserve,
205 nocheckspace=args.no_check_space)
206 # Write out the script to a file
207 with open(os.path.join(tmpdir, os.path.basename(tmpscript)), 'w') as f:
208 f.write(shellscript)
209 # Write out the file list
210 with open(os.path.join(tmpdir, os.path.basename(tmpfilelist)), 'w') as f:
211 f.write('%d\n' % ftotalsize)
212 for fpath, fsize in filelist:
213 f.write('%s %d\n' % (fpath, fsize))
214 # Copy them to the target
215 ret = subprocess.call("scp %s %s/* %s:%s" % (extraoptions, tmpdir, args.target, os.path.dirname(tmpscript)), shell=True)
216 if ret != 0:
217 raise DevtoolError('Failed to copy script to %s - rerun with -s to '
218 'get a complete error message' % args.target)
219 finally:
220 shutil.rmtree(tmpdir)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500221
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500222 # Now run the script
223 ret = exec_fakeroot(rd, 'tar cf - . | ssh %s %s \'sh %s %s %s %s\'' % (extraoptions, args.target, tmpscript, args.recipename, destdir, tmpfilelist), cwd=recipe_outdir, shell=True)
224 if ret != 0:
225 raise DevtoolError('Deploy failed - rerun with -s to get a complete '
226 'error message')
227
228 logger.info('Successfully deployed %s' % recipe_outdir)
229
230 files_list = []
231 for root, _, files in os.walk(recipe_outdir):
232 for filename in files:
233 filename = os.path.relpath(os.path.join(root, filename), recipe_outdir)
234 files_list.append(os.path.join(destdir, filename))
235
236 return 0
237
238def undeploy(args, config, basepath, workspace):
239 """Entry point for the devtool 'undeploy' subcommand"""
240 if args.all and args.recipename:
241 raise argparse_oe.ArgumentUsageError('Cannot specify -a/--all with a recipe name', 'undeploy-target')
242 elif not args.recipename and not args.all:
243 raise argparse_oe.ArgumentUsageError('If you don\'t specify a recipe, you must specify -a/--all', 'undeploy-target')
244
245 extraoptions = ''
246 if args.no_host_check:
247 extraoptions += '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
248 if not args.show_status:
249 extraoptions += ' -q'
250
251 args.target = args.target.split(':')[0]
252
253 tmpdir = tempfile.mkdtemp(prefix='devtool')
254 try:
255 tmpscript = '/tmp/devtool_undeploy.sh'
256 shellscript = _prepare_remote_script(deploy=False, dryrun=args.dry_run, undeployall=args.all)
257 # Write out the script to a file
258 with open(os.path.join(tmpdir, os.path.basename(tmpscript)), 'w') as f:
259 f.write(shellscript)
260 # Copy it to the target
261 ret = subprocess.call("scp %s %s/* %s:%s" % (extraoptions, tmpdir, args.target, os.path.dirname(tmpscript)), shell=True)
262 if ret != 0:
263 raise DevtoolError('Failed to copy script to %s - rerun with -s to '
264 'get a complete error message' % args.target)
265 finally:
266 shutil.rmtree(tmpdir)
267
268 # Now run the script
269 ret = subprocess.call('ssh %s %s \'sh %s %s\'' % (extraoptions, args.target, tmpscript, args.recipename), shell=True)
270 if ret != 0:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500271 raise DevtoolError('Undeploy failed - rerun with -s to get a complete '
272 'error message')
273
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500274 if not args.all and not args.dry_run:
275 logger.info('Successfully undeployed %s' % args.recipename)
276 return 0
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500277
278
279def register_commands(subparsers, context):
280 """Register devtool subcommands from the deploy plugin"""
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500281 parser_deploy = subparsers.add_parser('deploy-target',
282 help='Deploy recipe output files to live target machine',
283 description='Deploys a recipe\'s build output (i.e. the output of the do_install task) to a live target machine over ssh. By default, any existing files will be preserved instead of being overwritten and will be restored if you run devtool undeploy-target. Note: this only deploys the recipe itself and not any runtime dependencies, so it is assumed that those have been installed on the target beforehand.',
284 group='testbuild')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500285 parser_deploy.add_argument('recipename', help='Recipe to deploy')
286 parser_deploy.add_argument('target', help='Live target machine running an ssh server: user@hostname[:destdir]')
287 parser_deploy.add_argument('-c', '--no-host-check', help='Disable ssh host key checking', action='store_true')
288 parser_deploy.add_argument('-s', '--show-status', help='Show progress/status output', action='store_true')
289 parser_deploy.add_argument('-n', '--dry-run', help='List files to be deployed only', action='store_true')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500290 parser_deploy.add_argument('-p', '--no-preserve', help='Do not preserve existing files', action='store_true')
291 parser_deploy.add_argument('--no-check-space', help='Do not check for available space before deploying', action='store_true')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500292 parser_deploy.set_defaults(func=deploy)
293
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500294 parser_undeploy = subparsers.add_parser('undeploy-target',
295 help='Undeploy recipe output files in live target machine',
296 description='Un-deploys recipe output files previously deployed to a live target machine by devtool deploy-target.',
297 group='testbuild')
298 parser_undeploy.add_argument('recipename', help='Recipe to undeploy (if not using -a/--all)', nargs='?')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500299 parser_undeploy.add_argument('target', help='Live target machine running an ssh server: user@hostname')
300 parser_undeploy.add_argument('-c', '--no-host-check', help='Disable ssh host key checking', action='store_true')
301 parser_undeploy.add_argument('-s', '--show-status', help='Show progress/status output', action='store_true')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500302 parser_undeploy.add_argument('-a', '--all', help='Undeploy all recipes deployed on the target', action='store_true')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500303 parser_undeploy.add_argument('-n', '--dry-run', help='List files to be undeployed only', action='store_true')
304 parser_undeploy.set_defaults(func=undeploy)