blob: b3730ae833733667e169109579b214a198793b26 [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')
Patrick Williamsc0f7c042017-02-23 20:41:17 -060088 lines.append(r'freespace=$(df -P $checkpath | sed -nre "s/^(\S+\s+){3}([0-9]+).*/\2/p")')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050089 # 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:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600158 try:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500159 rd = tinfoil.parse_recipe(args.recipename)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600160 except Exception as e:
161 raise DevtoolError('Exception parsing recipe %s: %s' %
162 (args.recipename, e))
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500163 recipe_outdir = rd.getVar('D')
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600164 if not os.path.exists(recipe_outdir) or not os.listdir(recipe_outdir):
165 raise DevtoolError('No files to deploy - have you built the %s '
166 'recipe? If so, the install step has not installed '
167 'any files.' % args.recipename)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500168
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600169 filelist = []
170 ftotalsize = 0
171 for root, _, files in os.walk(recipe_outdir):
172 for fn in files:
173 # Get the size in kiB (since we'll be comparing it to the output of du -k)
174 # MUST use lstat() here not stat() or getfilesize() since we don't want to
175 # dereference symlinks
176 fsize = int(math.ceil(float(os.lstat(os.path.join(root, fn)).st_size)/1024))
177 ftotalsize += fsize
178 # The path as it would appear on the target
179 fpath = os.path.join(destdir, os.path.relpath(root, recipe_outdir), fn)
180 filelist.append((fpath, fsize))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500181
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600182 if args.dry_run:
183 print('Files to be deployed for %s on target %s:' % (args.recipename, args.target))
184 for item, _ in filelist:
185 print(' %s' % item)
186 return 0
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500187
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500188
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600189 extraoptions = ''
190 if args.no_host_check:
191 extraoptions += '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
192 if not args.show_status:
193 extraoptions += ' -q'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500194
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500195 scp_port = ''
196 ssh_port = ''
197 if not args.port:
198 raise DevtoolError("If you specify -P/--port then you must provide the port to be used to connect to the target")
199 else:
200 scp_port = "-P %s" % args.port
201 ssh_port = "-p %s" % args.port
202
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600203 # In order to delete previously deployed files and have the manifest file on
204 # the target, we write out a shell script and then copy it to the target
205 # so we can then run it (piping tar output to it).
206 # (We cannot use scp here, because it doesn't preserve symlinks.)
207 tmpdir = tempfile.mkdtemp(prefix='devtool')
208 try:
209 tmpscript = '/tmp/devtool_deploy.sh'
210 tmpfilelist = os.path.join(os.path.dirname(tmpscript), 'devtool_deploy.list')
211 shellscript = _prepare_remote_script(deploy=True,
212 verbose=args.show_status,
213 nopreserve=args.no_preserve,
214 nocheckspace=args.no_check_space)
215 # Write out the script to a file
216 with open(os.path.join(tmpdir, os.path.basename(tmpscript)), 'w') as f:
217 f.write(shellscript)
218 # Write out the file list
219 with open(os.path.join(tmpdir, os.path.basename(tmpfilelist)), 'w') as f:
220 f.write('%d\n' % ftotalsize)
221 for fpath, fsize in filelist:
222 f.write('%s %d\n' % (fpath, fsize))
223 # Copy them to the target
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500224 ret = subprocess.call("scp %s %s %s/* %s:%s" % (scp_port, extraoptions, tmpdir, args.target, os.path.dirname(tmpscript)), shell=True)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600225 if ret != 0:
226 raise DevtoolError('Failed to copy script to %s - rerun with -s to '
227 'get a complete error message' % args.target)
228 finally:
229 shutil.rmtree(tmpdir)
230
231 # Now run the script
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500232 ret = exec_fakeroot(rd, 'tar cf - . | ssh %s %s %s \'sh %s %s %s %s\'' % (ssh_port, extraoptions, args.target, tmpscript, args.recipename, destdir, tmpfilelist), cwd=recipe_outdir, shell=True)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500233 if ret != 0:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600234 raise DevtoolError('Deploy failed - rerun with -s to get a complete '
235 'error message')
236
237 logger.info('Successfully deployed %s' % recipe_outdir)
238
239 files_list = []
240 for root, _, files in os.walk(recipe_outdir):
241 for filename in files:
242 filename = os.path.relpath(os.path.join(root, filename), recipe_outdir)
243 files_list.append(os.path.join(destdir, filename))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500244 finally:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600245 tinfoil.shutdown()
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500246
247 return 0
248
249def undeploy(args, config, basepath, workspace):
250 """Entry point for the devtool 'undeploy' subcommand"""
251 if args.all and args.recipename:
252 raise argparse_oe.ArgumentUsageError('Cannot specify -a/--all with a recipe name', 'undeploy-target')
253 elif not args.recipename and not args.all:
254 raise argparse_oe.ArgumentUsageError('If you don\'t specify a recipe, you must specify -a/--all', 'undeploy-target')
255
256 extraoptions = ''
257 if args.no_host_check:
258 extraoptions += '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
259 if not args.show_status:
260 extraoptions += ' -q'
261
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500262 scp_port = ''
263 ssh_port = ''
264 if not args.port:
265 raise DevtoolError("If you specify -P/--port then you must provide the port to be used to connect to the target")
266 else:
267 scp_port = "-P %s" % args.port
268 ssh_port = "-p %s" % args.port
269
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500270 args.target = args.target.split(':')[0]
271
272 tmpdir = tempfile.mkdtemp(prefix='devtool')
273 try:
274 tmpscript = '/tmp/devtool_undeploy.sh'
275 shellscript = _prepare_remote_script(deploy=False, dryrun=args.dry_run, undeployall=args.all)
276 # Write out the script to a file
277 with open(os.path.join(tmpdir, os.path.basename(tmpscript)), 'w') as f:
278 f.write(shellscript)
279 # Copy it to the target
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500280 ret = subprocess.call("scp %s %s %s/* %s:%s" % (scp_port, extraoptions, tmpdir, args.target, os.path.dirname(tmpscript)), shell=True)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500281 if ret != 0:
282 raise DevtoolError('Failed to copy script to %s - rerun with -s to '
283 'get a complete error message' % args.target)
284 finally:
285 shutil.rmtree(tmpdir)
286
287 # Now run the script
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500288 ret = subprocess.call('ssh %s %s %s \'sh %s %s\'' % (ssh_port, extraoptions, args.target, tmpscript, args.recipename), shell=True)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500289 if ret != 0:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500290 raise DevtoolError('Undeploy failed - rerun with -s to get a complete '
291 'error message')
292
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500293 if not args.all and not args.dry_run:
294 logger.info('Successfully undeployed %s' % args.recipename)
295 return 0
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500296
297
298def register_commands(subparsers, context):
299 """Register devtool subcommands from the deploy plugin"""
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500300 parser_deploy = subparsers.add_parser('deploy-target',
301 help='Deploy recipe output files to live target machine',
302 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.',
303 group='testbuild')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500304 parser_deploy.add_argument('recipename', help='Recipe to deploy')
305 parser_deploy.add_argument('target', help='Live target machine running an ssh server: user@hostname[:destdir]')
306 parser_deploy.add_argument('-c', '--no-host-check', help='Disable ssh host key checking', action='store_true')
307 parser_deploy.add_argument('-s', '--show-status', help='Show progress/status output', action='store_true')
308 parser_deploy.add_argument('-n', '--dry-run', help='List files to be deployed only', action='store_true')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500309 parser_deploy.add_argument('-p', '--no-preserve', help='Do not preserve existing files', action='store_true')
310 parser_deploy.add_argument('--no-check-space', help='Do not check for available space before deploying', action='store_true')
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500311 parser_deploy.add_argument('-P', '--port', default='22', help='Port to use for connection to the target')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500312 parser_deploy.set_defaults(func=deploy)
313
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500314 parser_undeploy = subparsers.add_parser('undeploy-target',
315 help='Undeploy recipe output files in live target machine',
316 description='Un-deploys recipe output files previously deployed to a live target machine by devtool deploy-target.',
317 group='testbuild')
318 parser_undeploy.add_argument('recipename', help='Recipe to undeploy (if not using -a/--all)', nargs='?')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500319 parser_undeploy.add_argument('target', help='Live target machine running an ssh server: user@hostname')
320 parser_undeploy.add_argument('-c', '--no-host-check', help='Disable ssh host key checking', action='store_true')
321 parser_undeploy.add_argument('-s', '--show-status', help='Show progress/status output', action='store_true')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500322 parser_undeploy.add_argument('-a', '--all', help='Undeploy all recipes deployed on the target', action='store_true')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500323 parser_undeploy.add_argument('-n', '--dry-run', help='List files to be undeployed only', action='store_true')
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500324 parser_undeploy.add_argument('-P', '--port', default='22', help='Port to use for connection to the target')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500325 parser_undeploy.set_defaults(func=undeploy)