deploy.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. # Development tool - deploy/undeploy command plugin
  2. #
  3. # Copyright (C) 2014-2016 Intel Corporation
  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. import os
  19. import subprocess
  20. import logging
  21. import tempfile
  22. import shutil
  23. import argparse_oe
  24. from devtool import exec_fakeroot, setup_tinfoil, check_workspace_recipe, DevtoolError
  25. logger = logging.getLogger('devtool')
  26. deploylist_path = '/.devtool'
  27. def _prepare_remote_script(deploy, verbose=False, dryrun=False, undeployall=False, nopreserve=False, nocheckspace=False):
  28. """
  29. Prepare a shell script for running on the target to
  30. deploy/undeploy files. We have to be careful what we put in this
  31. script - only commands that are likely to be available on the
  32. target are suitable (the target might be constrained, e.g. using
  33. busybox rather than bash with coreutils).
  34. """
  35. lines = []
  36. lines.append('#!/bin/sh')
  37. lines.append('set -e')
  38. if undeployall:
  39. # Yes, I know this is crude - but it does work
  40. lines.append('for entry in %s/*.list; do' % deploylist_path)
  41. lines.append('[ ! -f $entry ] && exit')
  42. lines.append('set `basename $entry | sed "s/.list//"`')
  43. if dryrun:
  44. if not deploy:
  45. lines.append('echo "Previously deployed files for $1:"')
  46. lines.append('manifest="%s/$1.list"' % deploylist_path)
  47. lines.append('preservedir="%s/$1.preserve"' % deploylist_path)
  48. lines.append('if [ -f $manifest ] ; then')
  49. # Read manifest in reverse and delete files / remove empty dirs
  50. lines.append(' sed \'1!G;h;$!d\' $manifest | while read file')
  51. lines.append(' do')
  52. if dryrun:
  53. lines.append(' if [ ! -d $file ] ; then')
  54. lines.append(' echo $file')
  55. lines.append(' fi')
  56. else:
  57. lines.append(' if [ -d $file ] ; then')
  58. # Avoid deleting a preserved directory in case it has special perms
  59. lines.append(' if [ ! -d $preservedir/$file ] ; then')
  60. lines.append(' rmdir $file > /dev/null 2>&1 || true')
  61. lines.append(' fi')
  62. lines.append(' else')
  63. lines.append(' rm $file')
  64. lines.append(' fi')
  65. lines.append(' done')
  66. if not dryrun:
  67. lines.append(' rm $manifest')
  68. if not deploy and not dryrun:
  69. # May as well remove all traces
  70. lines.append(' rmdir `dirname $manifest` > /dev/null 2>&1 || true')
  71. lines.append('fi')
  72. if deploy:
  73. if not nocheckspace:
  74. # Check for available space
  75. # FIXME This doesn't take into account files spread across multiple
  76. # partitions, but doing that is non-trivial
  77. # Find the part of the destination path that exists
  78. lines.append('checkpath="$2"')
  79. lines.append('while [ "$checkpath" != "/" ] && [ ! -e $checkpath ]')
  80. lines.append('do')
  81. lines.append(' checkpath=`dirname "$checkpath"`')
  82. lines.append('done')
  83. lines.append(r'freespace=$(df -P $checkpath | sed -nre "s/^(\S+\s+){3}([0-9]+).*/\2/p")')
  84. # First line of the file is the total space
  85. lines.append('total=`head -n1 $3`')
  86. lines.append('if [ $total -gt $freespace ] ; then')
  87. lines.append(' echo "ERROR: insufficient space on target (available ${freespace}, needed ${total})"')
  88. lines.append(' exit 1')
  89. lines.append('fi')
  90. if not nopreserve:
  91. # Preserve any files that exist. Note that this will add to the
  92. # preserved list with successive deployments if the list of files
  93. # deployed changes, but because we've deleted any previously
  94. # deployed files at this point it will never preserve anything
  95. # that was deployed, only files that existed prior to any deploying
  96. # (which makes the most sense)
  97. lines.append('cat $3 | sed "1d" | while read file fsize')
  98. lines.append('do')
  99. lines.append(' if [ -e $file ] ; then')
  100. lines.append(' dest="$preservedir/$file"')
  101. lines.append(' mkdir -p `dirname $dest`')
  102. lines.append(' mv $file $dest')
  103. lines.append(' fi')
  104. lines.append('done')
  105. lines.append('rm $3')
  106. lines.append('mkdir -p `dirname $manifest`')
  107. lines.append('mkdir -p $2')
  108. if verbose:
  109. lines.append(' tar xv -C $2 -f - | tee $manifest')
  110. else:
  111. lines.append(' tar xv -C $2 -f - > $manifest')
  112. lines.append('sed -i "s!^./!$2!" $manifest')
  113. elif not dryrun:
  114. # Put any preserved files back
  115. lines.append('if [ -d $preservedir ] ; then')
  116. lines.append(' cd $preservedir')
  117. lines.append(' find . -type f -exec mv {} /{} \;')
  118. lines.append(' cd /')
  119. lines.append(' rm -rf $preservedir')
  120. lines.append('fi')
  121. if undeployall:
  122. if not dryrun:
  123. lines.append('echo "NOTE: Successfully undeployed $1"')
  124. lines.append('done')
  125. # Delete the script itself
  126. lines.append('rm $0')
  127. lines.append('')
  128. return '\n'.join(lines)
  129. def deploy(args, config, basepath, workspace):
  130. """Entry point for the devtool 'deploy' subcommand"""
  131. import re
  132. import math
  133. import oe.recipeutils
  134. check_workspace_recipe(workspace, args.recipename, checksrc=False)
  135. try:
  136. host, destdir = args.target.split(':')
  137. except ValueError:
  138. destdir = '/'
  139. else:
  140. args.target = host
  141. if not destdir.endswith('/'):
  142. destdir += '/'
  143. tinfoil = setup_tinfoil(basepath=basepath)
  144. try:
  145. try:
  146. rd = tinfoil.parse_recipe(args.recipename)
  147. except Exception as e:
  148. raise DevtoolError('Exception parsing recipe %s: %s' %
  149. (args.recipename, e))
  150. recipe_outdir = rd.getVar('D')
  151. if not os.path.exists(recipe_outdir) or not os.listdir(recipe_outdir):
  152. raise DevtoolError('No files to deploy - have you built the %s '
  153. 'recipe? If so, the install step has not installed '
  154. 'any files.' % args.recipename)
  155. filelist = []
  156. ftotalsize = 0
  157. for root, _, files in os.walk(recipe_outdir):
  158. for fn in files:
  159. # Get the size in kiB (since we'll be comparing it to the output of du -k)
  160. # MUST use lstat() here not stat() or getfilesize() since we don't want to
  161. # dereference symlinks
  162. fsize = int(math.ceil(float(os.lstat(os.path.join(root, fn)).st_size)/1024))
  163. ftotalsize += fsize
  164. # The path as it would appear on the target
  165. fpath = os.path.join(destdir, os.path.relpath(root, recipe_outdir), fn)
  166. filelist.append((fpath, fsize))
  167. if args.dry_run:
  168. print('Files to be deployed for %s on target %s:' % (args.recipename, args.target))
  169. for item, _ in filelist:
  170. print(' %s' % item)
  171. return 0
  172. extraoptions = ''
  173. if args.no_host_check:
  174. extraoptions += '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
  175. if not args.show_status:
  176. extraoptions += ' -q'
  177. # In order to delete previously deployed files and have the manifest file on
  178. # the target, we write out a shell script and then copy it to the target
  179. # so we can then run it (piping tar output to it).
  180. # (We cannot use scp here, because it doesn't preserve symlinks.)
  181. tmpdir = tempfile.mkdtemp(prefix='devtool')
  182. try:
  183. tmpscript = '/tmp/devtool_deploy.sh'
  184. tmpfilelist = os.path.join(os.path.dirname(tmpscript), 'devtool_deploy.list')
  185. shellscript = _prepare_remote_script(deploy=True,
  186. verbose=args.show_status,
  187. nopreserve=args.no_preserve,
  188. nocheckspace=args.no_check_space)
  189. # Write out the script to a file
  190. with open(os.path.join(tmpdir, os.path.basename(tmpscript)), 'w') as f:
  191. f.write(shellscript)
  192. # Write out the file list
  193. with open(os.path.join(tmpdir, os.path.basename(tmpfilelist)), 'w') as f:
  194. f.write('%d\n' % ftotalsize)
  195. for fpath, fsize in filelist:
  196. f.write('%s %d\n' % (fpath, fsize))
  197. # Copy them to the target
  198. ret = subprocess.call("scp %s %s/* %s:%s" % (extraoptions, tmpdir, args.target, os.path.dirname(tmpscript)), shell=True)
  199. if ret != 0:
  200. raise DevtoolError('Failed to copy script to %s - rerun with -s to '
  201. 'get a complete error message' % args.target)
  202. finally:
  203. shutil.rmtree(tmpdir)
  204. # Now run the script
  205. 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)
  206. if ret != 0:
  207. raise DevtoolError('Deploy failed - rerun with -s to get a complete '
  208. 'error message')
  209. logger.info('Successfully deployed %s' % recipe_outdir)
  210. files_list = []
  211. for root, _, files in os.walk(recipe_outdir):
  212. for filename in files:
  213. filename = os.path.relpath(os.path.join(root, filename), recipe_outdir)
  214. files_list.append(os.path.join(destdir, filename))
  215. finally:
  216. tinfoil.shutdown()
  217. return 0
  218. def undeploy(args, config, basepath, workspace):
  219. """Entry point for the devtool 'undeploy' subcommand"""
  220. if args.all and args.recipename:
  221. raise argparse_oe.ArgumentUsageError('Cannot specify -a/--all with a recipe name', 'undeploy-target')
  222. elif not args.recipename and not args.all:
  223. raise argparse_oe.ArgumentUsageError('If you don\'t specify a recipe, you must specify -a/--all', 'undeploy-target')
  224. extraoptions = ''
  225. if args.no_host_check:
  226. extraoptions += '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
  227. if not args.show_status:
  228. extraoptions += ' -q'
  229. args.target = args.target.split(':')[0]
  230. tmpdir = tempfile.mkdtemp(prefix='devtool')
  231. try:
  232. tmpscript = '/tmp/devtool_undeploy.sh'
  233. shellscript = _prepare_remote_script(deploy=False, dryrun=args.dry_run, undeployall=args.all)
  234. # Write out the script to a file
  235. with open(os.path.join(tmpdir, os.path.basename(tmpscript)), 'w') as f:
  236. f.write(shellscript)
  237. # Copy it to the target
  238. ret = subprocess.call("scp %s %s/* %s:%s" % (extraoptions, tmpdir, args.target, os.path.dirname(tmpscript)), shell=True)
  239. if ret != 0:
  240. raise DevtoolError('Failed to copy script to %s - rerun with -s to '
  241. 'get a complete error message' % args.target)
  242. finally:
  243. shutil.rmtree(tmpdir)
  244. # Now run the script
  245. ret = subprocess.call('ssh %s %s \'sh %s %s\'' % (extraoptions, args.target, tmpscript, args.recipename), shell=True)
  246. if ret != 0:
  247. raise DevtoolError('Undeploy failed - rerun with -s to get a complete '
  248. 'error message')
  249. if not args.all and not args.dry_run:
  250. logger.info('Successfully undeployed %s' % args.recipename)
  251. return 0
  252. def register_commands(subparsers, context):
  253. """Register devtool subcommands from the deploy plugin"""
  254. parser_deploy = subparsers.add_parser('deploy-target',
  255. help='Deploy recipe output files to live target machine',
  256. 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.',
  257. group='testbuild')
  258. parser_deploy.add_argument('recipename', help='Recipe to deploy')
  259. parser_deploy.add_argument('target', help='Live target machine running an ssh server: user@hostname[:destdir]')
  260. parser_deploy.add_argument('-c', '--no-host-check', help='Disable ssh host key checking', action='store_true')
  261. parser_deploy.add_argument('-s', '--show-status', help='Show progress/status output', action='store_true')
  262. parser_deploy.add_argument('-n', '--dry-run', help='List files to be deployed only', action='store_true')
  263. parser_deploy.add_argument('-p', '--no-preserve', help='Do not preserve existing files', action='store_true')
  264. parser_deploy.add_argument('--no-check-space', help='Do not check for available space before deploying', action='store_true')
  265. parser_deploy.set_defaults(func=deploy)
  266. parser_undeploy = subparsers.add_parser('undeploy-target',
  267. help='Undeploy recipe output files in live target machine',
  268. description='Un-deploys recipe output files previously deployed to a live target machine by devtool deploy-target.',
  269. group='testbuild')
  270. parser_undeploy.add_argument('recipename', help='Recipe to undeploy (if not using -a/--all)', nargs='?')
  271. parser_undeploy.add_argument('target', help='Live target machine running an ssh server: user@hostname')
  272. parser_undeploy.add_argument('-c', '--no-host-check', help='Disable ssh host key checking', action='store_true')
  273. parser_undeploy.add_argument('-s', '--show-status', help='Show progress/status output', action='store_true')
  274. parser_undeploy.add_argument('-a', '--all', help='Undeploy all recipes deployed on the target', action='store_true')
  275. parser_undeploy.add_argument('-n', '--dry-run', help='List files to be undeployed only', action='store_true')
  276. parser_undeploy.set_defaults(func=undeploy)