chi-appliance.py 9.9 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
    session = cloud_config.get_session()

    # blazar acces
33 34 35
    # for some reason the blazar client ignore the session region
    return blazar_client.Client(1, session=session, service_type='reservation',
                                region_name=region)
36 37 38 39


def do_create(argv):
    """Create an appliance inside a lease, based on template."""
40 41
    shade_client = get_shade_client(argv.region)
    blazar_client = get_blazar_client(argv.region)
42 43 44 45 46 47 48 49 50 51 52 53 54 55
    # 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,
56
                                        wait=argv.wait, **extra_args)
57 58 59 60 61 62 63
        print(json.dumps(ret, indent=4))
    except shade.exc.OpenStackCloudHTTPError as e:
        print(e)


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


def do_show(argv):
    """Show appliance with <name>."""
74
    shade_client = get_shade_client(argv.region)
75 76 77 78 79 80 81 82 83
    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."""
84
    shade_client = get_shade_client(argv.region)
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
    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.
    """
102 103
    shade_client = get_shade_client(argv.region)
    blazar_client = get_blazar_client(argv.region)
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
    # 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')
149 150
    # remove potential ./ansible/ from playbook path
    playpath = os.path.split(argv.playbook)[1]
151 152
    playbook = ('---\n'
                '- hosts: all\n'
153
                '  gather_facts: no\n'
154
                '  tasks:\n'
155 156 157 158
                '    - name: Wait for frontend\n'
                '      wait_for_connection:\n'
                '    - name: Gather info about frontend\n'
                '      setup:\n'
159 160 161 162 163 164 165 166 167 168
                '    - 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'
169
                '        src: ' + confpath + '\n'
170 171
                '        dest: ~/\n'
                '    - name: Execute ansible on frontend\n'
172
                '      shell: ANSIBLE_HOST_KEY_CHECKING=False'
173
                ' ansible-playbook -i inventory.yaml ' + playpath + '\n'
174 175 176 177 178
                '      args:\n'
                '        chdir: ~/ansible\n'
                '      register: config\n'
                '    - debug: var=config.stdout_lines')
    # generate files
179
    remote_inv_path = os.path.join(confpath, "inventory.yaml")
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
    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()
197
            if err is None:
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
                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')
214 215
    parser.add_argument('--region', default=os.environ.get('OS_REGION_NAME'),
                        help='Region name (in clouds.yaml)')
216 217 218 219 220 221 222
    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")
223 224
    parser_create.add_argument("--wait", action='store_true',
                               help="Wait for the operation to complete")
225 226 227 228 229 230 231 232 233
    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")
234 235
    parser_delete.add_argument("--wait", action='store_true',
                               help="Wait for the operation to complete")
236 237 238 239 240 241 242 243 244 245 246 247 248
    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")
249 250
    parser_config.add_argument("playbook", default="main.yaml", nargs='?',
                               help="Playbook for remote configuration")
251 252 253 254 255 256 257 258 259 260 261 262
    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()