blob: c4c7bf6c732cd01dbd61d6a23862ac3f311804a4 [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:
159 rd = oe.recipeutils.parse_recipe_simple(tinfoil.cooker, args.recipename, tinfoil.config_data)
160 except Exception as e:
161 raise DevtoolError('Exception parsing recipe %s: %s' %
162 (args.recipename, e))
163 recipe_outdir = rd.getVar('D', True)
164 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
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600195 # In order to delete previously deployed files and have the manifest file on
196 # the target, we write out a shell script and then copy it to the target
197 # so we can then run it (piping tar output to it).
198 # (We cannot use scp here, because it doesn't preserve symlinks.)
199 tmpdir = tempfile.mkdtemp(prefix='devtool')
200 try:
201 tmpscript = '/tmp/devtool_deploy.sh'
202 tmpfilelist = os.path.join(os.path.dirname(tmpscript), 'devtool_deploy.list')
203 shellscript = _prepare_remote_script(deploy=True,
204 verbose=args.show_status,
205 nopreserve=args.no_preserve,
206 nocheckspace=args.no_check_space)
207 # Write out the script to a file
208 with open(os.path.join(tmpdir, os.path.basename(tmpscript)), 'w') as f:
209 f.write(shellscript)
210 # Write out the file list
211 with open(os.path.join(tmpdir, os.path.basename(tmpfilelist)), 'w') as f:
212 f.write('%d\n' % ftotalsize)
213 for fpath, fsize in filelist:
214 f.write('%s %d\n' % (fpath, fsize))
215 # Copy them to the target
216 ret = subprocess.call("scp %s %s/* %s:%s" % (extraoptions, tmpdir, args.target, os.path.dirname(tmpscript)), shell=True)
217 if ret != 0:
218 raise DevtoolError('Failed to copy script to %s - rerun with -s to '
219 'get a complete error message' % args.target)
220 finally:
221 shutil.rmtree(tmpdir)
222
223 # Now run the script
224 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)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500225 if ret != 0:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600226 raise DevtoolError('Deploy failed - rerun with -s to get a complete '
227 'error message')
228
229 logger.info('Successfully deployed %s' % recipe_outdir)
230
231 files_list = []
232 for root, _, files in os.walk(recipe_outdir):
233 for filename in files:
234 filename = os.path.relpath(os.path.join(root, filename), recipe_outdir)
235 files_list.append(os.path.join(destdir, filename))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500236 finally:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600237 tinfoil.shutdown()
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500238
239 return 0
240
241def undeploy(args, config, basepath, workspace):
242 """Entry point for the devtool 'undeploy' subcommand"""
243 if args.all and args.recipename:
244 raise argparse_oe.ArgumentUsageError('Cannot specify -a/--all with a recipe name', 'undeploy-target')
245 elif not args.recipename and not args.all:
246 raise argparse_oe.ArgumentUsageError('If you don\'t specify a recipe, you must specify -a/--all', 'undeploy-target')
247
248 extraoptions = ''
249 if args.no_host_check:
250 extraoptions += '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
251 if not args.show_status:
252 extraoptions += ' -q'
253
254 args.target = args.target.split(':')[0]
255
256 tmpdir = tempfile.mkdtemp(prefix='devtool')
257 try:
258 tmpscript = '/tmp/devtool_undeploy.sh'
259 shellscript = _prepare_remote_script(deploy=False, dryrun=args.dry_run, undeployall=args.all)
260 # Write out the script to a file
261 with open(os.path.join(tmpdir, os.path.basename(tmpscript)), 'w') as f:
262 f.write(shellscript)
263 # Copy it to the target
264 ret = subprocess.call("scp %s %s/* %s:%s" % (extraoptions, tmpdir, args.target, os.path.dirname(tmpscript)), shell=True)
265 if ret != 0:
266 raise DevtoolError('Failed to copy script to %s - rerun with -s to '
267 'get a complete error message' % args.target)
268 finally:
269 shutil.rmtree(tmpdir)
270
271 # Now run the script
272 ret = subprocess.call('ssh %s %s \'sh %s %s\'' % (extraoptions, args.target, tmpscript, args.recipename), shell=True)
273 if ret != 0:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500274 raise DevtoolError('Undeploy failed - rerun with -s to get a complete '
275 'error message')
276
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500277 if not args.all and not args.dry_run:
278 logger.info('Successfully undeployed %s' % args.recipename)
279 return 0
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500280
281
282def register_commands(subparsers, context):
283 """Register devtool subcommands from the deploy plugin"""
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500284 parser_deploy = subparsers.add_parser('deploy-target',
285 help='Deploy recipe output files to live target machine',
286 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.',
287 group='testbuild')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500288 parser_deploy.add_argument('recipename', help='Recipe to deploy')
289 parser_deploy.add_argument('target', help='Live target machine running an ssh server: user@hostname[:destdir]')
290 parser_deploy.add_argument('-c', '--no-host-check', help='Disable ssh host key checking', action='store_true')
291 parser_deploy.add_argument('-s', '--show-status', help='Show progress/status output', action='store_true')
292 parser_deploy.add_argument('-n', '--dry-run', help='List files to be deployed only', action='store_true')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500293 parser_deploy.add_argument('-p', '--no-preserve', help='Do not preserve existing files', action='store_true')
294 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 -0500295 parser_deploy.set_defaults(func=deploy)
296
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500297 parser_undeploy = subparsers.add_parser('undeploy-target',
298 help='Undeploy recipe output files in live target machine',
299 description='Un-deploys recipe output files previously deployed to a live target machine by devtool deploy-target.',
300 group='testbuild')
301 parser_undeploy.add_argument('recipename', help='Recipe to undeploy (if not using -a/--all)', nargs='?')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500302 parser_undeploy.add_argument('target', help='Live target machine running an ssh server: user@hostname')
303 parser_undeploy.add_argument('-c', '--no-host-check', help='Disable ssh host key checking', action='store_true')
304 parser_undeploy.add_argument('-s', '--show-status', help='Show progress/status output', action='store_true')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500305 parser_undeploy.add_argument('-a', '--all', help='Undeploy all recipes deployed on the target', action='store_true')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500306 parser_undeploy.add_argument('-n', '--dry-run', help='List files to be undeployed only', action='store_true')
307 parser_undeploy.set_defaults(func=undeploy)