#!/usr/bin/env python import argparse import json import logging import os import subprocess from tempfile import NamedTemporaryFile import yaml import os_client_config from blazarclient import client as blazar_client import shade def get_cloud_config(region=None): """Retrieve the config in clouds.yaml.""" config = os_client_config.OpenStackConfig() return config.get_one('chameleon', region_name=region) def get_shade_client(region=None): cloud = get_cloud_config(region) return shade.OpenStackCloud(cloud_config=cloud) def get_blazar_client(region=None): """Retrieve a client to blazar based on clouds.yaml config.""" cloud_config = get_cloud_config(region) session = cloud_config.get_session() # blazar acces # for some reason the blazar client ignore the session region return blazar_client.Client(1, session=session, service_type='reservation', region_name=region) def execute_cmd(args, env_update={}, prefix=''): """Execute a command and read stdout and stderr on the fly.""" print("Executing:", " ".join(args)) newenv = os.environ newenv.update(env_update) proc = subprocess.Popen(args, env=newenv, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) while True: err = proc.poll() if err is None: line = proc.stdout.readline() if line: print(prefix, line, end='', flush=True) else: if err == 0: print(prefix, "Command successful.") else: for l in proc.stderr.readlines(): print(l, end='', flush=True) print(prefix, "Command failed.") break def do_create(argv): """Create an appliance inside a lease, based on template.""" shade_client = get_shade_client(argv.region) blazar_client = get_blazar_client(argv.region) # build common parameters leases = blazar_client.lease.list() leases = [l for l in leases if l['name'] == argv.lease] if not leases: print("ERROR: lease", argv.lease, "does not exists.") return reservation = leases[0]['reservations'][0] extra_args = dict() extra_args.update(argv.extra) extra_args['reservation_id'] = reservation['id'] extra_args['node_count'] = reservation['max'] # add metadata to the servers lease_meta = {'lease_name': argv.lease, 'lease_id': leases[0]['id']} extra_args['server_meta'] = json.dumps(lease_meta) template = os.path.abspath(argv.template) try: ret = shade_client.create_stack(argv.name, template_file=template, wait=argv.wait, **extra_args) print(json.dumps(ret, indent=4)) except shade.exc.OpenStackCloudHTTPError as e: print(e) def do_delete(argv): """Delete an appliance with .""" shade_client = get_shade_client(argv.region) ret = shade_client.delete_stack(argv.name, wait=argv.wait) if ret: print("Appliance successfully deleted.") else: print("Appliance not found.") def do_show(argv): """Show appliance with .""" shade_client = get_shade_client(argv.region) app = shade_client.get_stack(argv.name) if app: print(json.dumps(app, indent=4)) else: print("No appliance with this name:", argv.name) def do_list(argv): """List all appliances.""" shade_client = get_shade_client(argv.region) app_list = shade_client.list_stacks() if app_list: print(json.dumps(app_list, indent=4)) else: print("No appliances.") def do_configure(argv): """Copy the ansible configuration to the frontend node and launch it. We use ansible-playbook to perform this. This function basically generates 2 inventories and a playbook: - one inventory with how to connect to the stack frontend. - one inventory with stack information from inside the stack - one playbook to copy the ansible config over to the frontend and launch ansible inside. """ shade_client = get_shade_client(argv.region) blazar_client = get_blazar_client(argv.region) # basic info appliance = shade_client.get_stack(argv.name) if not appliance: print("ERROR: missing appliance.") return lease_list = blazar_client.lease.list() for l in lease_list: reservation = l['reservations'][0] if reservation['id'] == appliance['parameters']['reservation_id']: lease = l break else: print("ERROR: Could not find lease for appliance.") return # local inventory creation. Only need to be able to connect to frontend local_inventory = {'all': {'hosts': {'frontend': {'ansible_ssh_host': "", 'ansible_ssh_user': "cc"} }}} for out in appliance['outputs']: if out['output_key'] == 'first_instance_ip': public_ip = out['output_value'] break else: print("ERROR: missing first_instance_ip from appliance output") return local_inventory['all']['hosts']['frontend']['ansible_ssh_host'] = public_ip # appliance inventory, need to grab info about hostnames and private_ip of # all nodes remote_inventory = {'all': {'hosts': {}}} server_list = shade_client.list_servers() for s in server_list: meta = s.get('metadata') if not meta or meta.get('lease_id') != lease['id']: continue name = s['name'] for i in s['addresses']['sharednet1']: if i['OS-EXT-IPS:type'] == 'fixed': ip = i['addr'] break else: print("ERROR: server", name, "does not have a private IP") return remote_inventory['all']['hosts'][name] = {'name': name, 'ansible_host': ip} # local playbook mypath = os.path.abspath(os.path.dirname(__file__)) confpath = os.path.join(mypath, 'ansible') # remove potential ./ansible/ from playbook path playpath = os.path.split(argv.playbook)[1] playbook = ('---\n' '- hosts: all\n' ' gather_facts: no\n' ' tasks:\n' ' - name: Wait for frontend\n' ' wait_for_connection:\n' ' - name: Gather info about frontend\n' ' setup:\n' ' - name: Ensure dependencies are installed\n' ' package:\n' ' name: "{{ item }}"\n' ' state: present\n' ' with_items:\n' ' - ansible\n' ' - rsync\n' ' become: yes\n' ' - name: Copy ansible configuration to the frontend\n' ' synchronize:\n' ' src: ' + confpath + '\n' ' dest: ~/\n') # generate files remote_inv_path = os.path.join(confpath, "inventory.yaml") local_temp = NamedTemporaryFile(mode='w+', encoding='utf8', delete=False) play_temp = NamedTemporaryFile(mode='w+', encoding='utf8', delete=False) with open(remote_inv_path, "w+", encoding='utf8') as remote_inv: yaml.dump(remote_inventory, stream=remote_inv) with local_temp, play_temp: yaml.dump(local_inventory, stream=local_temp) play_temp.write(playbook) # call the local ansible try: cmd = ["ansible-playbook", "-i", local_temp.name, play_temp.name] execute_cmd(cmd, {'ANSIBLE_HOST_KEY_CHECKING': 'False'}, 'localhost:') finally: os.unlink(local_temp.name) os.unlink(play_temp.name) # call the remote ansible remote_cmd = ('cd ansible;' 'ANSIBLE_HOST_KEY_CHECKING=False ' 'ansible-playbook -i inventory.yaml' ) if argv.vault: remote_cmd += ' --ask-vault-pass' remote_cmd += ' ' + playpath # forward agent, auto accept host, don't save it in the host file cmd = ["ssh", "-A", "-oUserKnownHostsFile=/dev/null", "-oStrictHostKeyChecking=no", "cc@" + public_ip, remote_cmd] execute_cmd(cmd, prefix='frontend:') def main(): parser = argparse.ArgumentParser(description='Chameleon Appliance Helper') parser.add_argument('--region', default=os.environ.get('OS_REGION_NAME'), help='Region name (in clouds.yaml)') parser.add_argument('--debug', help="Print debugging output", action='store_true') subparsers = parser.add_subparsers(title='Commands', dest='command') subparsers.required = True # create a lease parser_create = subparsers.add_parser("create", help="Create an appliance") parser_create.add_argument("--wait", action='store_true', help="Wait for the operation to complete") parser_create.add_argument("name", help="Name of the appliance") parser_create.add_argument("lease", help="Lease for the appliance") parser_create.add_argument("template", help="Appliance template") parser_create.add_argument("extra", help="JSON dict of extra template parameters", default=dict(), type=json.loads) parser_create.set_defaults(func=do_create) parser_delete = subparsers.add_parser("delete", help="Delete an appliance") parser_delete.add_argument("--wait", action='store_true', help="Wait for the operation to complete") parser_delete.add_argument("name", help="Name of the appliance") parser_delete.set_defaults(func=do_delete) parser_show = subparsers.add_parser("show", help="Show an appliance") parser_show.add_argument("name", help="Name of the appliance") parser_show.set_defaults(func=do_show) parser_list = subparsers.add_parser("list", help="List all appliances") parser_list.set_defaults(func=do_list) parser_config = subparsers.add_parser("configure", help="Configure an appliance") parser_config.add_argument("--vault", action='store_true', help="Ask for vault password on frontend play") parser_config.add_argument("name", help="Name of the appliance") parser_config.add_argument("playbook", default="main.yaml", nargs='?', help="Playbook for remote configuration") parser_config.set_defaults(func=do_configure) args = parser.parse_args() if args.debug: logger = logging.getLogger('keystoneauth') logger.addHandler(logging.StreamHandler()) logger.setLevel(logging.DEBUG) args.func(args) if __name__ == '__main__': main()