def ssh_config(host_string=None): """ Return ssh configuration dict for current env.host_string host value. Memoizes the loaded SSH config file, but not the specific per-host results. This function performs the necessary "is SSH config enabled?" checks and will simply return an empty dict if not. If SSH config *is* enabled and the value of env.ssh_config_path is not a valid file, it will abort. May give an explicit host string as ``host_string``. """ from fabric.state import env if not env.use_ssh_config: return {} if '_ssh_config' not in env: try: conf = ssh.SSHConfig() path = os.path.expanduser(env.ssh_config_path) with open(path) as fd: conf.parse(fd) env._ssh_config = conf except IOError: abort("Unable to load SSH config file '%s'" % path) host = parse_host_string(host_string or env.host_string)['host'] return env._ssh_config.lookup(host)
def build_server(app, thisbuild, vcs, build_dir, output_dir, sdk_path, force): """Do a build on the build server.""" import ssh # Reset existing builder machine to a clean state if possible. vm_ok = False if not options.resetserver: print "Checking for valid existing build server" if (os.path.exists(os.path.join('builder', 'Vagrantfile')) and os.path.exists(os.path.join('builder', '.vagrant'))): print "...VM is present" p = subprocess.Popen([ 'VBoxManage', 'snapshot', get_builder_vm_id(), 'list', '--details' ], cwd='builder', stdout=subprocess.PIPE) output = p.communicate()[0] if output.find('fdroidclean') != -1: print "...snapshot exists - resetting build server to clean state" p = subprocess.Popen(['vagrant', 'status'], cwd='builder', stdout=subprocess.PIPE) output = p.communicate()[0] if output.find('running') != -1: print "...suspending" subprocess.call(['vagrant', 'suspend'], cwd='builder') if subprocess.call([ 'VBoxManage', 'snapshot', get_builder_vm_id(), 'restore', 'fdroidclean' ], cwd='builder') == 0: print "...reset to snapshot - server is valid" if subprocess.call(['vagrant', 'up'], cwd='builder') != 0: raise BuildException("Failed to start build server") vm_ok = True else: print "...failed to reset to snapshot" else: print "...snapshot doesn't exist - vagrant snap said:\n" + output # If we can't use the existing machine for any reason, make a # new one from scratch. if not vm_ok: if os.path.exists('builder'): print "Removing broken/incomplete/unwanted build server" subprocess.call(['vagrant', 'destroy', '-f'], cwd='builder') shutil.rmtree('builder') os.mkdir('builder') with open('builder/Vagrantfile', 'w') as vf: vf.write('Vagrant::Config.run do |config|\n') vf.write('config.vm.box = "buildserver"\n') vf.write( 'config.vm.customize ["modifyvm", :id, "--memory", "768"]\n') vf.write('end\n') print "Starting new build server" if subprocess.call(['vagrant', 'up'], cwd='builder') != 0: raise BuildException("Failed to start build server") # Open SSH connection to make sure it's working and ready... print "Connecting to virtual machine..." if subprocess.call('vagrant ssh-config >sshconfig', cwd='builder', shell=True) != 0: raise BuildException("Error getting ssh config") vagranthost = 'default' # Host in ssh config file sshconfig = ssh.SSHConfig() sshf = open('builder/sshconfig', 'r') sshconfig.parse(sshf) sshf.close() sshconfig = sshconfig.lookup(vagranthost) sshs = ssh.SSHClient() sshs.set_missing_host_key_policy(ssh.AutoAddPolicy()) idfile = sshconfig['identityfile'] if idfile.startswith('"') and idfile.endswith('"'): idfile = idfile[1:-1] sshs.connect(sshconfig['hostname'], username=sshconfig['user'], port=int(sshconfig['port']), timeout=300, look_for_keys=False, key_filename=idfile) sshs.close() print "Saving clean state of new build server" subprocess.call(['vagrant', 'suspend'], cwd='builder') if subprocess.call([ 'VBoxManage', 'snapshot', get_builder_vm_id(), 'take', 'fdroidclean' ], cwd='builder') != 0: raise BuildException("Failed to take snapshot") print "Restarting new build server" if subprocess.call(['vagrant', 'up'], cwd='builder') != 0: raise BuildException("Failed to start build server") # Make sure it worked... p = subprocess.Popen([ 'VBoxManage', 'snapshot', get_builder_vm_id(), 'list', '--details' ], cwd='builder', stdout=subprocess.PIPE) output = p.communicate()[0] if output.find('fdroidclean') == -1: raise BuildException("Failed to take snapshot.") try: # Get SSH configuration settings for us to connect... print "Getting ssh configuration..." subprocess.call('vagrant ssh-config >sshconfig', cwd='builder', shell=True) vagranthost = 'default' # Host in ssh config file # Load and parse the SSH config... sshconfig = ssh.SSHConfig() sshf = open('builder/sshconfig', 'r') sshconfig.parse(sshf) sshf.close() sshconfig = sshconfig.lookup(vagranthost) # Open SSH connection... print "Connecting to virtual machine..." sshs = ssh.SSHClient() sshs.set_missing_host_key_policy(ssh.AutoAddPolicy()) idfile = sshconfig['identityfile'] if idfile.startswith('"') and idfile.endswith('"'): idfile = idfile[1:-1] sshs.connect(sshconfig['hostname'], username=sshconfig['user'], port=int(sshconfig['port']), timeout=300, look_for_keys=False, key_filename=idfile) # Get an SFTP connection... ftp = sshs.open_sftp() ftp.get_channel().settimeout(15) # Put all the necessary files in place... ftp.chdir('/home/vagrant') # Helper to copy the contents of a directory to the server... def send_dir(path): root = os.path.dirname(path) main = os.path.basename(path) ftp.mkdir(main) for r, d, f in os.walk(path): rr = os.path.relpath(r, root) ftp.chdir(rr) for dd in d: ftp.mkdir(dd) for ff in f: if not os.path.islink(os.path.join(root, rr, ff)): ftp.put(os.path.join(root, rr, ff), ff) for i in range(len(rr.split('/'))): ftp.chdir('..') ftp.chdir('..') print "Preparing server for build..." serverpath = os.path.abspath(os.path.dirname(__file__)) ftp.put(os.path.join(serverpath, 'build.py'), 'build.py') ftp.put(os.path.join(serverpath, 'common.py'), 'common.py') ftp.put(os.path.join(serverpath, '..', 'config.buildserver.py'), 'config.py') # Copy the metadata - just the file for this app... ftp.mkdir('metadata') ftp.mkdir('srclibs') ftp.chdir('metadata') ftp.put(os.path.join('metadata', app['id'] + '.txt'), app['id'] + '.txt') # And patches if there are any... if os.path.exists(os.path.join('metadata', app['id'])): send_dir(os.path.join('metadata', app['id'])) ftp.chdir('/home/vagrant') # Create the build directory... ftp.mkdir('build') ftp.chdir('build') ftp.mkdir('extlib') ftp.mkdir('srclib') # Copy the main app source code if os.path.exists(build_dir): send_dir(build_dir) # Copy any extlibs that are required... if 'extlibs' in thisbuild: ftp.chdir('/home/vagrant/build/extlib') for lib in thisbuild['extlibs'].split(';'): lp = lib.split('/') for d in lp[:-1]: if d not in ftp.listdir(): ftp.mkdir(d) ftp.chdir(d) ftp.put(os.path.join('build/extlib', lib), lp[-1]) for _ in lp[:-1]: ftp.chdir('..') # Copy any srclibs that are required... srclibpaths = [] if 'srclibs' in thisbuild: for lib in thisbuild['srclibs'].split(';'): name, _ = lib.split('@') if options.verbose: print "Processing srclib '" + name + "'" srclibpaths.append((name, common.getsrclib(lib, 'build/srclib', sdk_path, basepath=True, prepare=False))) # If one was used for the main source, add that too. basesrclib = vcs.getsrclib() if basesrclib: srclibpaths.append(basesrclib) for name, lib in srclibpaths: print "Sending srclib '" + lib + "'" ftp.chdir('/home/vagrant/build/srclib') if not os.path.exists(lib): raise BuildException("Missing srclib directory '" + lib + "'") send_dir(lib) # Copy the metadata file too... ftp.chdir('/home/vagrant/srclibs') ftp.put(os.path.join('srclibs', name + '.txt'), name + '.txt') # Execute the build script... print "Starting build..." chan = sshs.get_transport().open_session() cmdline = 'python build.py --on-server' if force: cmdline += ' --force --test' cmdline += ' -p ' + app['id'] + ' --vercode ' + thisbuild['vercode'] chan.exec_command(cmdline) output = '' error = '' while not chan.exit_status_ready(): while chan.recv_ready(): output += chan.recv(1024) while chan.recv_stderr_ready(): error += chan.recv_stderr(1024) print "...getting exit status" returncode = chan.recv_exit_status() while chan.recv_ready(): output += chan.recv(1024) while chan.recv_stderr_ready(): error += chan.recv_stderr(1024) if returncode != 0: raise BuildException( "Build.py failed on server for %s:%s" % (app['id'], thisbuild['version']), output.strip(), error.strip()) # Retrieve the built files... print "Retrieving build output..." if force: ftp.chdir('/home/vagrant/tmp') else: ftp.chdir('/home/vagrant/unsigned') apkfile = app['id'] + '_' + thisbuild['vercode'] + '.apk' tarball = app['id'] + '_' + thisbuild['vercode'] + '_src' + '.tar.gz' try: ftp.get(apkfile, os.path.join(output_dir, apkfile)) ftp.get(tarball, os.path.join(output_dir, tarball)) except: raise BuildException( "Build failed for %s:%s" % (app['id'], thisbuild['version']), output.strip(), error.strip()) ftp.close() finally: # Suspend the build server. print "Suspending build server" subprocess.call(['vagrant', 'suspend'], cwd='builder')
def main(args=None): parser = argparse.ArgumentParser(add_help=False, description=''' Command line utility for packaging and installing vagrant boxes built by executing fabric tasks. %(prog)s is implemented as a thin wrapper around fabric's 'fab' command. It creates a temporary vagrant environment, configures fabric to connect to it, then hands off execution to fabric, which runs its tasks against the environment. When finished, it packages and/or installs the built box according to the arguments provided.''') meta = parser.add_argument_group(title='optional arguments') meta.add_argument( '-h', '--help', action='store_true', help='''Show this help message and exit. For help with fabric options, run 'fab --help'.''') meta.add_argument('-V', '--version', action='store_true', help='''show program's version number and exit''') meta.add_argument('--log-level', help='Level for logging program output', default='warning', type=log_level) # Primary arguments for basebox command. main = parser.add_argument_group( title='build arguments', description='''Arguments to configure building, installation, and packaging''') main.add_argument( '--base', help='''Vagrant base box to build with. Defaults to vagrant's precise64 box, also available at https://files.vagrantup.com/precise64.box''', default='https://files.vagrantup.com/precise64.box') main.add_argument( '--vagrantfile-template', # help='Jinja template for rendering Vagrantfile to build with', help=argparse.SUPPRESS, default=TEMPLATE_ENV.get_template('Vagrantfile.default'), type=TEMPLATE_ENV.get_template) main.add_argument('--package-as', metavar='PACKAGE_FILE', help='Package file to write the build result to.') main.add_argument( '--package-vagrantfile', help='''Specify how/whether to set the output package Vagrantfile. The default is 'inherit', which packages with the Vagrantfile of the base box if it exists. Specifying 'none' will create the package without a Vagrantfile. If a file path or raw string is provided, its contents will be used as the output package Vagrantfile. ''', default='inherit', type=package_vagrantfile, metavar='(VFILE_PATH|VFILE_STRING|inherit|none)') main.add_argument('--install-as', help='Install the built box to vagrant as BOXNAME', metavar='BOXNAME') # Arguments shared with fabric shared = parser.add_argument_group( title='shared arguments', description='''These arguments are arguments to fabric, but %(prog)s also uses them to configure its environment. They are passed through to fabric when it executes.''') shared.add_argument( '-i', metavar='PATH', help='''Path to SSH private key file. May be repeated. Added to the generated Vagrantfile as the 'config.ssh.private_key_path' option. '''), shared.add_argument( '-u', '--user', help='''Username to use when connecting to vagrant hosts. Added to the generated Vagrantfile as the 'config.ssh.username' option. '''), shared.add_argument( '--port', help='''SSH connection port. Added to the generated Vagrantfile as the 'config.ssh.port' option''') # OK, this one doesn't do anything extra, but we still want to sanity-check # the fabfile before executing. shared.add_argument('-f', '--fabfile', help=argparse.SUPPRESS) # The 'hosts' parameter works a bit differently in basebox - each hostname # must be a valid identifier name and can be assigned roles by listing them # in the format 'host:role1,role2'. # # >>> basebox -H box1:web box2:db,cache main.add_argument('-H', '--hosts', help='', nargs='+', type=host_with_roles, action='append') args, fab_args = parser.parse_known_args(args=args) # Set log level so everything after this can emit proper logs LOG.level = args.log_level # Remove the separator from the fabric arguments if '--' in fab_args: fab_args.remove('--') # Flatten host-role entries and map bidirectionally host_roles = {} roledefs = {} for host, roles in [tpl for sublist in args.hosts for tpl in sublist]: if roles: LOG.info('Host <%s> specified with roles: %s' % (host, roles)) else: LOG.info('Host <%s> specified with no roles.' % host) host_roles[host] = roles for role in roles: roledefs.setdefault(role, []).append(host) # Add hosts to fabric args fab_args[:0] = ['--hosts', ','.join(host_roles.keys())] # Duplicate shared args back into the fabric argument list for action in shared._group_actions: if getattr(args, action.dest, None): flag = ('-%s' if len(action.dest) == 1 else '--%s') % action.dest fab_args[:0] = [flag, getattr(args, action.dest)] LOG.info('Checking fabric parameters: %s' % fab_args) if args.help: parser.print_help() elif args.version: print_version() else: if not (args.install_as or args.package_as): print 'No action specified (you should use --install-as or --package-as).' else: # Replace sys.argv to simulate calling fabric as a CLI script argv_original = sys.argv stderr_original = sys.stderr sys.argv = ['fab'] + fab_args sys.stderr = StringIO.StringIO() # Sanity-check fabric arguments before doing anything heavy try: fabric.main.parse_options() except SystemExit: print( 'An error was encountered while trying to parse the ' 'arguments to fabric:') print '' print os.linesep.join( '\t%s' % line for line in sys.stderr.getvalue().splitlines()[2:]) print '' print 'Please check the syntax and try again.' raise finally: sys.stderr = stderr_original # Check fabfile resolution fabfile_original = fabric.state.env.fabfile if args.fabfile: fabric.state.env.fabfile = args.fabfile if not fabric.main.find_fabfile(): print( "Fabric couldn't find any fabfiles! (You may want to " "change directories or specify the -f option)") raise SystemExit fabric.state.env.fabfile = fabfile_original # Render input Vagrantfile with appropriate context vfile_ctx = { 'base': args.base, 'hosts': host_roles.keys(), 'ssh': { 'username': args.user, 'private_key_path': args.i, 'port': args.port } } mode_local() with tempbox(base=args.base, vfile_template=args.vagrantfile_template, vfile_template_context=vfile_ctx) as default_box: context = default_box.context # Fabricate an SSH config for the vagrant environment and add # it to fabric.state.env ssh_conf = ssh.SSHConfig() ssh_configs = ssh_conf._config # Add a host entry to SSH config for each vagrant box for box_name in context.list_boxes(): box = context[box_name] box.up() ssh_settings = box.ssh_config() ssh_settings.update({ 'host': box_name, 'stricthostkeychecking': 'no' }) ssh_configs.append(ssh_settings) fabric.api.settings.use_ssh_config = True fabric.api.env._ssh_config = ssh_conf # Configure roledefs fabric.state.env.roledefs = roledefs # Hand execution over to fabric with mode_remote(): try: fabric.main.main() except SystemExit as e: # Raised when fabric finishes. if e.code is not 0: LOG.error('Fabric exited with error code %s' % e.code) raise pass vfile = resolve_package_vagrantfile(args.package_vagrantfile) context.package(vagrantfile=vfile, install_as=args.install_as, output=args.package_as) sys.argv = argv_original