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