| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1 | # Development tool - deploy/undeploy command plugin | 
 | 2 | # | 
| Patrick Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 3 | # Copyright (C) 2014-2016 Intel Corporation | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 4 | # | 
 | 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 |  | 
 | 19 | import os | 
 | 20 | import subprocess | 
 | 21 | import logging | 
| Patrick Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 22 | import tempfile | 
 | 23 | import shutil | 
 | 24 | import argparse_oe | 
| Patrick Williams | f1e5d69 | 2016-03-30 15:21:19 -0500 | [diff] [blame] | 25 | from devtool import exec_fakeroot, setup_tinfoil, check_workspace_recipe, DevtoolError | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 26 |  | 
 | 27 | logger = logging.getLogger('devtool') | 
 | 28 |  | 
| Patrick Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 29 | deploylist_path = '/.devtool' | 
 | 30 |  | 
 | 31 | def _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 Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 88 |             lines.append(r'freespace=$(df -P $checkpath | sed -nre "s/^(\S+\s+){3}([0-9]+).*/\2/p")') | 
| Patrick Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 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 Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 139 | def deploy(args, config, basepath, workspace): | 
 | 140 |     """Entry point for the devtool 'deploy' subcommand""" | 
 | 141 |     import re | 
| Patrick Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 142 |     import math | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 143 |     import oe.recipeutils | 
 | 144 |  | 
| Patrick Williams | f1e5d69 | 2016-03-30 15:21:19 -0500 | [diff] [blame] | 145 |     check_workspace_recipe(workspace, args.recipename, checksrc=False) | 
 | 146 |  | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 147 |     try: | 
 | 148 |         host, destdir = args.target.split(':') | 
 | 149 |     except ValueError: | 
 | 150 |         destdir = '/' | 
 | 151 |     else: | 
 | 152 |         args.target = host | 
| Patrick Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 153 |     if not destdir.endswith('/'): | 
 | 154 |         destdir += '/' | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 155 |  | 
| Patrick Williams | f1e5d69 | 2016-03-30 15:21:19 -0500 | [diff] [blame] | 156 |     tinfoil = setup_tinfoil(basepath=basepath) | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 157 |     try: | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 158 |         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 Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 168 |  | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 169 |         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 Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 181 |  | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 182 |         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 Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 187 |  | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 188 |  | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 189 |         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 Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 194 |  | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 195 |         # 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 Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 225 |         if ret != 0: | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 226 |             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 Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 236 |     finally: | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 237 |         tinfoil.shutdown() | 
| Patrick Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 238 |  | 
 | 239 |     return 0 | 
 | 240 |  | 
 | 241 | def 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 Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 274 |         raise DevtoolError('Undeploy failed - rerun with -s to get a complete ' | 
 | 275 |                            'error message') | 
 | 276 |  | 
| Patrick Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 277 |     if not args.all and not args.dry_run: | 
 | 278 |         logger.info('Successfully undeployed %s' % args.recipename) | 
 | 279 |     return 0 | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 280 |  | 
 | 281 |  | 
 | 282 | def register_commands(subparsers, context): | 
 | 283 |     """Register devtool subcommands from the deploy plugin""" | 
| Patrick Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 284 |     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 Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 288 |     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 Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 293 |     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 Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 295 |     parser_deploy.set_defaults(func=deploy) | 
 | 296 |  | 
| Patrick Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 297 |     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 Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 302 |     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 Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 305 |     parser_undeploy.add_argument('-a', '--all', help='Undeploy all recipes deployed on the target', action='store_true') | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 306 |     parser_undeploy.add_argument('-n', '--dry-run', help='List files to be undeployed only', action='store_true') | 
 | 307 |     parser_undeploy.set_defaults(func=undeploy) |