blob: d1ce2309f9642976a8648fd45f669fd72a5d172d [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#
Brad Bishopc342db32019-05-15 21:57:59 -04005# SPDX-License-Identifier: GPL-2.0-only
Patrick Williamsc124f4f2015-09-15 14:41:29 -05006#
Patrick Williamsc124f4f2015-09-15 14:41:29 -05007"""Devtool plugin containing the deploy subcommands"""
8
Patrick Williamsc124f4f2015-09-15 14:41:29 -05009import logging
Brad Bishopd7bf8c12018-02-25 22:55:05 -050010import os
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050011import shutil
Brad Bishopd7bf8c12018-02-25 22:55:05 -050012import subprocess
13import tempfile
14
15import bb.utils
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050016import argparse_oe
Brad Bishopd7bf8c12018-02-25 22:55:05 -050017import oe.types
18
Patrick Williamsf1e5d692016-03-30 15:21:19 -050019from devtool import exec_fakeroot, setup_tinfoil, check_workspace_recipe, DevtoolError
Patrick Williamsc124f4f2015-09-15 14:41:29 -050020
21logger = logging.getLogger('devtool')
22
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050023deploylist_path = '/.devtool'
24
25def _prepare_remote_script(deploy, verbose=False, dryrun=False, undeployall=False, nopreserve=False, nocheckspace=False):
26 """
27 Prepare a shell script for running on the target to
28 deploy/undeploy files. We have to be careful what we put in this
29 script - only commands that are likely to be available on the
30 target are suitable (the target might be constrained, e.g. using
31 busybox rather than bash with coreutils).
32 """
33 lines = []
34 lines.append('#!/bin/sh')
35 lines.append('set -e')
36 if undeployall:
37 # Yes, I know this is crude - but it does work
38 lines.append('for entry in %s/*.list; do' % deploylist_path)
39 lines.append('[ ! -f $entry ] && exit')
40 lines.append('set `basename $entry | sed "s/.list//"`')
41 if dryrun:
42 if not deploy:
43 lines.append('echo "Previously deployed files for $1:"')
44 lines.append('manifest="%s/$1.list"' % deploylist_path)
45 lines.append('preservedir="%s/$1.preserve"' % deploylist_path)
46 lines.append('if [ -f $manifest ] ; then')
47 # Read manifest in reverse and delete files / remove empty dirs
48 lines.append(' sed \'1!G;h;$!d\' $manifest | while read file')
49 lines.append(' do')
50 if dryrun:
51 lines.append(' if [ ! -d $file ] ; then')
52 lines.append(' echo $file')
53 lines.append(' fi')
54 else:
55 lines.append(' if [ -d $file ] ; then')
56 # Avoid deleting a preserved directory in case it has special perms
57 lines.append(' if [ ! -d $preservedir/$file ] ; then')
58 lines.append(' rmdir $file > /dev/null 2>&1 || true')
59 lines.append(' fi')
60 lines.append(' else')
Brad Bishopd7bf8c12018-02-25 22:55:05 -050061 lines.append(' rm -f $file')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050062 lines.append(' fi')
63 lines.append(' done')
64 if not dryrun:
65 lines.append(' rm $manifest')
66 if not deploy and not dryrun:
67 # May as well remove all traces
68 lines.append(' rmdir `dirname $manifest` > /dev/null 2>&1 || true')
69 lines.append('fi')
70
71 if deploy:
72 if not nocheckspace:
73 # Check for available space
74 # FIXME This doesn't take into account files spread across multiple
75 # partitions, but doing that is non-trivial
76 # Find the part of the destination path that exists
77 lines.append('checkpath="$2"')
78 lines.append('while [ "$checkpath" != "/" ] && [ ! -e $checkpath ]')
79 lines.append('do')
80 lines.append(' checkpath=`dirname "$checkpath"`')
81 lines.append('done')
Patrick Williamsc0f7c042017-02-23 20:41:17 -060082 lines.append(r'freespace=$(df -P $checkpath | sed -nre "s/^(\S+\s+){3}([0-9]+).*/\2/p")')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050083 # First line of the file is the total space
84 lines.append('total=`head -n1 $3`')
85 lines.append('if [ $total -gt $freespace ] ; then')
86 lines.append(' echo "ERROR: insufficient space on target (available ${freespace}, needed ${total})"')
87 lines.append(' exit 1')
88 lines.append('fi')
89 if not nopreserve:
90 # Preserve any files that exist. Note that this will add to the
91 # preserved list with successive deployments if the list of files
92 # deployed changes, but because we've deleted any previously
93 # deployed files at this point it will never preserve anything
94 # that was deployed, only files that existed prior to any deploying
95 # (which makes the most sense)
96 lines.append('cat $3 | sed "1d" | while read file fsize')
97 lines.append('do')
98 lines.append(' if [ -e $file ] ; then')
99 lines.append(' dest="$preservedir/$file"')
100 lines.append(' mkdir -p `dirname $dest`')
101 lines.append(' mv $file $dest')
102 lines.append(' fi')
103 lines.append('done')
104 lines.append('rm $3')
105 lines.append('mkdir -p `dirname $manifest`')
106 lines.append('mkdir -p $2')
107 if verbose:
108 lines.append(' tar xv -C $2 -f - | tee $manifest')
109 else:
110 lines.append(' tar xv -C $2 -f - > $manifest')
111 lines.append('sed -i "s!^./!$2!" $manifest')
112 elif not dryrun:
113 # Put any preserved files back
114 lines.append('if [ -d $preservedir ] ; then')
115 lines.append(' cd $preservedir')
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500116 # find from busybox might not have -exec, so we don't use that
117 lines.append(' find . -type f | while read file')
118 lines.append(' do')
119 lines.append(' mv $file /$file')
120 lines.append(' done')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500121 lines.append(' cd /')
122 lines.append(' rm -rf $preservedir')
123 lines.append('fi')
124
125 if undeployall:
126 if not dryrun:
127 lines.append('echo "NOTE: Successfully undeployed $1"')
128 lines.append('done')
129
130 # Delete the script itself
131 lines.append('rm $0')
132 lines.append('')
133
134 return '\n'.join(lines)
135
136
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500137
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500138def deploy(args, config, basepath, workspace):
139 """Entry point for the devtool 'deploy' subcommand"""
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500140 import math
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500141 import oe.recipeutils
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500142 import oe.package
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500143
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500144 check_workspace_recipe(workspace, args.recipename, checksrc=False)
145
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500146 try:
147 host, destdir = args.target.split(':')
148 except ValueError:
149 destdir = '/'
150 else:
151 args.target = host
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500152 if not destdir.endswith('/'):
153 destdir += '/'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500154
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500155 tinfoil = setup_tinfoil(basepath=basepath)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500156 try:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600157 try:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500158 rd = tinfoil.parse_recipe(args.recipename)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600159 except Exception as e:
160 raise DevtoolError('Exception parsing recipe %s: %s' %
161 (args.recipename, e))
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500162 recipe_outdir = rd.getVar('D')
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600163 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)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500167
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500168 if args.strip and not args.dry_run:
169 # Fakeroot copy to new destination
170 srcdir = recipe_outdir
171 recipe_outdir = os.path.join(rd.getVar('WORKDIR'), 'deploy-target-stripped')
172 if os.path.isdir(recipe_outdir):
173 bb.utils.remove(recipe_outdir, True)
174 exec_fakeroot(rd, "cp -af %s %s" % (os.path.join(srcdir, '.'), recipe_outdir), shell=True)
175 os.environ['PATH'] = ':'.join([os.environ['PATH'], rd.getVar('PATH') or ''])
176 oe.package.strip_execs(args.recipename, recipe_outdir, rd.getVar('STRIP'), rd.getVar('libdir'),
Brad Bishopa5c52ff2018-11-23 10:55:50 +1300177 rd.getVar('base_libdir'), rd)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500178
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600179 filelist = []
180 ftotalsize = 0
181 for root, _, files in os.walk(recipe_outdir):
182 for fn in files:
183 # Get the size in kiB (since we'll be comparing it to the output of du -k)
184 # MUST use lstat() here not stat() or getfilesize() since we don't want to
185 # dereference symlinks
186 fsize = int(math.ceil(float(os.lstat(os.path.join(root, fn)).st_size)/1024))
187 ftotalsize += fsize
188 # The path as it would appear on the target
189 fpath = os.path.join(destdir, os.path.relpath(root, recipe_outdir), fn)
190 filelist.append((fpath, fsize))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500191
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600192 if args.dry_run:
193 print('Files to be deployed for %s on target %s:' % (args.recipename, args.target))
194 for item, _ in filelist:
195 print(' %s' % item)
196 return 0
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500197
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600198 extraoptions = ''
199 if args.no_host_check:
200 extraoptions += '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
201 if not args.show_status:
202 extraoptions += ' -q'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500203
Brad Bishop19323692019-04-05 15:28:33 -0400204 scp_sshexec = ''
205 ssh_sshexec = 'ssh'
206 if args.ssh_exec:
207 scp_sshexec = "-S %s" % args.ssh_exec
208 ssh_sshexec = args.ssh_exec
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500209 scp_port = ''
210 ssh_port = ''
Brad Bishop316dfdd2018-06-25 12:45:53 -0400211 if args.port:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500212 scp_port = "-P %s" % args.port
213 ssh_port = "-p %s" % args.port
214
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600215 # In order to delete previously deployed files and have the manifest file on
216 # the target, we write out a shell script and then copy it to the target
217 # so we can then run it (piping tar output to it).
218 # (We cannot use scp here, because it doesn't preserve symlinks.)
219 tmpdir = tempfile.mkdtemp(prefix='devtool')
220 try:
221 tmpscript = '/tmp/devtool_deploy.sh'
222 tmpfilelist = os.path.join(os.path.dirname(tmpscript), 'devtool_deploy.list')
223 shellscript = _prepare_remote_script(deploy=True,
224 verbose=args.show_status,
225 nopreserve=args.no_preserve,
226 nocheckspace=args.no_check_space)
227 # Write out the script to a file
228 with open(os.path.join(tmpdir, os.path.basename(tmpscript)), 'w') as f:
229 f.write(shellscript)
230 # Write out the file list
231 with open(os.path.join(tmpdir, os.path.basename(tmpfilelist)), 'w') as f:
232 f.write('%d\n' % ftotalsize)
233 for fpath, fsize in filelist:
234 f.write('%s %d\n' % (fpath, fsize))
235 # Copy them to the target
Brad Bishop19323692019-04-05 15:28:33 -0400236 ret = subprocess.call("scp %s %s %s %s/* %s:%s" % (scp_sshexec, scp_port, extraoptions, tmpdir, args.target, os.path.dirname(tmpscript)), shell=True)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600237 if ret != 0:
238 raise DevtoolError('Failed to copy script to %s - rerun with -s to '
239 'get a complete error message' % args.target)
240 finally:
241 shutil.rmtree(tmpdir)
242
243 # Now run the script
Brad Bishop19323692019-04-05 15:28:33 -0400244 ret = exec_fakeroot(rd, 'tar cf - . | %s %s %s %s \'sh %s %s %s %s\'' % (ssh_sshexec, ssh_port, extraoptions, args.target, tmpscript, args.recipename, destdir, tmpfilelist), cwd=recipe_outdir, shell=True)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500245 if ret != 0:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600246 raise DevtoolError('Deploy failed - rerun with -s to get a complete '
247 'error message')
248
249 logger.info('Successfully deployed %s' % recipe_outdir)
250
251 files_list = []
252 for root, _, files in os.walk(recipe_outdir):
253 for filename in files:
254 filename = os.path.relpath(os.path.join(root, filename), recipe_outdir)
255 files_list.append(os.path.join(destdir, filename))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500256 finally:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600257 tinfoil.shutdown()
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500258
259 return 0
260
261def undeploy(args, config, basepath, workspace):
262 """Entry point for the devtool 'undeploy' subcommand"""
263 if args.all and args.recipename:
264 raise argparse_oe.ArgumentUsageError('Cannot specify -a/--all with a recipe name', 'undeploy-target')
265 elif not args.recipename and not args.all:
266 raise argparse_oe.ArgumentUsageError('If you don\'t specify a recipe, you must specify -a/--all', 'undeploy-target')
267
268 extraoptions = ''
269 if args.no_host_check:
270 extraoptions += '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
271 if not args.show_status:
272 extraoptions += ' -q'
273
Brad Bishop19323692019-04-05 15:28:33 -0400274 scp_sshexec = ''
275 ssh_sshexec = 'ssh'
276 if args.ssh_exec:
277 scp_sshexec = "-S %s" % args.ssh_exec
278 ssh_sshexec = args.ssh_exec
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500279 scp_port = ''
280 ssh_port = ''
Brad Bishop316dfdd2018-06-25 12:45:53 -0400281 if args.port:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500282 scp_port = "-P %s" % args.port
283 ssh_port = "-p %s" % args.port
284
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500285 args.target = args.target.split(':')[0]
286
287 tmpdir = tempfile.mkdtemp(prefix='devtool')
288 try:
289 tmpscript = '/tmp/devtool_undeploy.sh'
290 shellscript = _prepare_remote_script(deploy=False, dryrun=args.dry_run, undeployall=args.all)
291 # Write out the script to a file
292 with open(os.path.join(tmpdir, os.path.basename(tmpscript)), 'w') as f:
293 f.write(shellscript)
294 # Copy it to the target
Brad Bishop19323692019-04-05 15:28:33 -0400295 ret = subprocess.call("scp %s %s %s %s/* %s:%s" % (scp_sshexec, scp_port, extraoptions, tmpdir, args.target, os.path.dirname(tmpscript)), shell=True)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500296 if ret != 0:
297 raise DevtoolError('Failed to copy script to %s - rerun with -s to '
298 'get a complete error message' % args.target)
299 finally:
300 shutil.rmtree(tmpdir)
301
302 # Now run the script
Brad Bishop19323692019-04-05 15:28:33 -0400303 ret = subprocess.call('%s %s %s %s \'sh %s %s\'' % (ssh_sshexec, ssh_port, extraoptions, args.target, tmpscript, args.recipename), shell=True)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500304 if ret != 0:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500305 raise DevtoolError('Undeploy failed - rerun with -s to get a complete '
306 'error message')
307
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500308 if not args.all and not args.dry_run:
309 logger.info('Successfully undeployed %s' % args.recipename)
310 return 0
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500311
312
313def register_commands(subparsers, context):
314 """Register devtool subcommands from the deploy plugin"""
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500315
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500316 parser_deploy = subparsers.add_parser('deploy-target',
317 help='Deploy recipe output files to live target machine',
318 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.',
319 group='testbuild')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500320 parser_deploy.add_argument('recipename', help='Recipe to deploy')
321 parser_deploy.add_argument('target', help='Live target machine running an ssh server: user@hostname[:destdir]')
322 parser_deploy.add_argument('-c', '--no-host-check', help='Disable ssh host key checking', action='store_true')
323 parser_deploy.add_argument('-s', '--show-status', help='Show progress/status output', action='store_true')
324 parser_deploy.add_argument('-n', '--dry-run', help='List files to be deployed only', action='store_true')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500325 parser_deploy.add_argument('-p', '--no-preserve', help='Do not preserve existing files', action='store_true')
326 parser_deploy.add_argument('--no-check-space', help='Do not check for available space before deploying', action='store_true')
Brad Bishop19323692019-04-05 15:28:33 -0400327 parser_deploy.add_argument('-e', '--ssh-exec', help='Executable to use in place of ssh')
Brad Bishop316dfdd2018-06-25 12:45:53 -0400328 parser_deploy.add_argument('-P', '--port', help='Specify port to use for connection to the target')
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500329
330 strip_opts = parser_deploy.add_mutually_exclusive_group(required=False)
331 strip_opts.add_argument('-S', '--strip',
332 help='Strip executables prior to deploying (default: %(default)s). '
333 'The default value of this option can be controlled by setting the strip option in the [Deploy] section to True or False.',
334 default=oe.types.boolean(context.config.get('Deploy', 'strip', default='0')),
335 action='store_true')
336 strip_opts.add_argument('--no-strip', help='Do not strip executables prior to deploy', dest='strip', action='store_false')
337
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500338 parser_deploy.set_defaults(func=deploy)
339
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500340 parser_undeploy = subparsers.add_parser('undeploy-target',
341 help='Undeploy recipe output files in live target machine',
342 description='Un-deploys recipe output files previously deployed to a live target machine by devtool deploy-target.',
343 group='testbuild')
344 parser_undeploy.add_argument('recipename', help='Recipe to undeploy (if not using -a/--all)', nargs='?')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500345 parser_undeploy.add_argument('target', help='Live target machine running an ssh server: user@hostname')
346 parser_undeploy.add_argument('-c', '--no-host-check', help='Disable ssh host key checking', action='store_true')
347 parser_undeploy.add_argument('-s', '--show-status', help='Show progress/status output', action='store_true')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500348 parser_undeploy.add_argument('-a', '--all', help='Undeploy all recipes deployed on the target', action='store_true')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500349 parser_undeploy.add_argument('-n', '--dry-run', help='List files to be undeployed only', action='store_true')
Brad Bishop19323692019-04-05 15:28:33 -0400350 parser_undeploy.add_argument('-e', '--ssh-exec', help='Executable to use in place of ssh')
Brad Bishop316dfdd2018-06-25 12:45:53 -0400351 parser_undeploy.add_argument('-P', '--port', help='Specify port to use for connection to the target')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500352 parser_undeploy.set_defaults(func=undeploy)