#!/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 return blazar_client.Client(1, session=session, service_type='reservation') 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'] 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({'az': 'blazar_'+lease['id']}) for s in server_list: 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') playbook = ('---\n' '- hosts: all\n' ' tasks:\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' ' - name: Generate ssh-key for appliance\n' ' user:\n' ' name: cc\n' ' state: present\n' ' generate_ssh_key: yes\n' ' - name: Execute ansible on frontend\n' ' command: ansible-playbook -i inventory main.yaml\n' ' args:\n' ' chdir: ~/ansible\n' ' register: config\n' ' - debug: var=config.stdout_lines') # generate files # BAD ERROR HANDLING HERE remote_inv_path = "./ansible/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) try: # call ansible aargs = ["ansible-playbook", "-i", local_temp.name, play_temp.name] proc = subprocess.Popen(aargs, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) while True: err = proc.poll() if err is None: print(proc.stdout.readline(), end='', flush=True) else: if err == 0: print("Configuration successful.") else: for l in proc.stderr.readlines(): print(l, end='', flush=True) print("Configuration failed.") break finally: os.unlink(local_temp.name) os.unlink(play_temp.name) 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("name", help="Name of the appliance") 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()