chi-appliance.py 9.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
#!/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


16
def get_cloud_config(region=None):
17 18
    """Retrieve the config in clouds.yaml."""
    config = os_client_config.OpenStackConfig()
19
    return config.get_one('chameleon', region_name=region)
20 21


22 23
def get_shade_client(region=None):
    cloud = get_cloud_config(region)
24 25 26
    return shade.OpenStackCloud(cloud_config=cloud)


27
def get_blazar_client(region=None):
28
    """Retrieve a client to blazar based on clouds.yaml config."""
29
    cloud_config = get_cloud_config(region)
30 31 32 33 34 35 36 37
    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."""
38 39
    shade_client = get_shade_client(argv.region)
    blazar_client = get_blazar_client(argv.region)
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
    # 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=True, **extra_args)
        print(json.dumps(ret, indent=4))
    except shade.exc.OpenStackCloudHTTPError as e:
        print(e)


def do_delete(argv):
    """Delete an appliance with <name>."""
62
    shade_client = get_shade_client(argv.region)
63 64 65 66 67 68 69 70 71
    ret = shade_client.delete_stack(argv.name)
    if ret:
        print("Appliance successfully deleted.")
    else:
        print("Appliance not found.")


def do_show(argv):
    """Show appliance with <name>."""
72
    shade_client = get_shade_client(argv.region)
73 74 75 76 77 78 79 80 81
    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."""
82
    shade_client = get_shade_client(argv.region)
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
    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.
    """
100 101
    shade_client = get_shade_client(argv.region)
    blazar_client = get_blazar_client(argv.region)
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
    # 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()
193
            if err is None:
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
                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')
210 211
    parser.add_argument('--region', default=os.environ.get('OS_REGION_NAME'),
                        help='Region name (in clouds.yaml)')
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
    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("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("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()