chi-appliance.py 10.1 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
    # 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']
53 54 55
    # add metadata to the servers
    lease_meta = {'lease_name': argv.lease, 'lease_id': leases[0]['id']}
    extra_args['server_meta'] = json.dumps(lease_meta)
56 57 58
    template = os.path.abspath(argv.template)
    try:
        ret = shade_client.create_stack(argv.name, template_file=template,
59
                                        wait=argv.wait, **extra_args)
60 61 62 63 64 65 66
        print(json.dumps(ret, indent=4))
    except shade.exc.OpenStackCloudHTTPError as e:
        print(e)


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


def do_show(argv):
    """Show appliance with <name>."""
77
    shade_client = get_shade_client(argv.region)
78 79 80 81 82 83 84 85 86
    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."""
87
    shade_client = get_shade_client(argv.region)
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
    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.
    """
105 106
    shade_client = get_shade_client(argv.region)
    blazar_client = get_blazar_client(argv.region)
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
    # 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': {}}}
137
    server_list = shade_client.list_servers()
138
    for s in server_list:
139 140 141 142
        meta = s.get('metadata')
        if not meta or meta.get('lease_id') != lease['id']:
            continue

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