chi-appliance.py 10.7 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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
def execute_cmd(args, env_update={}, prefix=''):
    """Execute a command and read stdout and stderr on the fly."""
    print("Executing:", " ".join(args))
    newenv = os.environ
    newenv.update(env_update)
    proc = subprocess.Popen(args, env=newenv, stdout=subprocess.PIPE,
                            stderr=subprocess.PIPE, universal_newlines=True)
    while True:
        err = proc.poll()
        if err is None:
            line = proc.stdout.readline()
            if line:
                print(prefix, line, end='', flush=True)
        else:
            if err == 0:
                print(prefix, "Command successful.")
            else:
                for l in proc.stderr.readlines():
                    print(l, end='', flush=True)
                print(prefix, "Command failed.")
            break


61 62
def do_create(argv):
    """Create an appliance inside a lease, based on template."""
63 64
    shade_client = get_shade_client(argv.region)
    blazar_client = get_blazar_client(argv.region)
65 66 67 68 69 70 71 72 73 74 75
    # 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']
76 77 78
    # add metadata to the servers
    lease_meta = {'lease_name': argv.lease, 'lease_id': leases[0]['id']}
    extra_args['server_meta'] = json.dumps(lease_meta)
79 80 81
    template = os.path.abspath(argv.template)
    try:
        ret = shade_client.create_stack(argv.name, template_file=template,
82
                                        wait=argv.wait, **extra_args)
83 84 85 86 87 88 89
        print(json.dumps(ret, indent=4))
    except shade.exc.OpenStackCloudHTTPError as e:
        print(e)


def do_delete(argv):
    """Delete an appliance with <name>."""
90
    shade_client = get_shade_client(argv.region)
91
    ret = shade_client.delete_stack(argv.name, wait=argv.wait)
92 93 94 95 96 97 98 99
    if ret:
        print("Appliance successfully deleted.")
    else:
        print("Appliance not found.")


def do_show(argv):
    """Show appliance with <name>."""
100
    shade_client = get_shade_client(argv.region)
101 102 103 104 105 106 107 108 109
    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."""
110
    shade_client = get_shade_client(argv.region)
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
    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.
    """
128 129
    shade_client = get_shade_client(argv.region)
    blazar_client = get_blazar_client(argv.region)
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
    # 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': {}}}
160
    server_list = shade_client.list_servers()
161
    for s in server_list:
162 163 164 165
        meta = s.get('metadata')
        if not meta or meta.get('lease_id') != lease['id']:
            continue

166 167 168 169 170 171 172 173 174 175 176 177 178
        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')
179 180
    # remove potential ./ansible/ from playbook path
    playpath = os.path.split(argv.playbook)[1]
181 182
    playbook = ('---\n'
                '- hosts: all\n'
183
                '  gather_facts: no\n'
184
                '  tasks:\n'
185 186 187 188
                '    - name: Wait for frontend\n'
                '      wait_for_connection:\n'
                '    - name: Gather info about frontend\n'
                '      setup:\n'
189 190 191 192 193 194 195 196 197 198
                '    - 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'
199
                '        src: ' + confpath + '\n'
200
                '        dest: ~/\n')
201
    # generate files
202
    remote_inv_path = os.path.join(confpath, "inventory.yaml")
203 204 205 206 207 208 209 210 211
    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)

212
    # call the local ansible
213
    try:
214 215
        cmd = ["ansible-playbook", "-i", local_temp.name, play_temp.name]
        execute_cmd(cmd, {'ANSIBLE_HOST_KEY_CHECKING': 'False'}, 'localhost:')
216 217 218 219
    finally:
        os.unlink(local_temp.name)
        os.unlink(play_temp.name)

220 221 222
    # call the remote ansible
    remote_cmd = ('cd ansible;'
                  'ANSIBLE_HOST_KEY_CHECKING=False '
223 224 225 226 227
                  'ansible-playbook -i inventory.yaml'
                  )
    if argv.vault:
        remote_cmd += ' --ask-vault-pass'
    remote_cmd += ' ' + playpath
228 229 230 231 232
    # forward agent, auto accept host, don't save it in the host file
    cmd = ["ssh", "-A", "-oUserKnownHostsFile=/dev/null",
           "-oStrictHostKeyChecking=no", "cc@" + public_ip, remote_cmd]
    execute_cmd(cmd, prefix='frontend:')

233 234 235

def main():
    parser = argparse.ArgumentParser(description='Chameleon Appliance Helper')
236 237
    parser.add_argument('--region', default=os.environ.get('OS_REGION_NAME'),
                        help='Region name (in clouds.yaml)')
238 239 240 241 242 243 244
    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")
245 246
    parser_create.add_argument("--wait", action='store_true',
                               help="Wait for the operation to complete")
247 248 249 250 251 252 253 254 255
    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")
256 257
    parser_delete.add_argument("--wait", action='store_true',
                               help="Wait for the operation to complete")
258 259 260 261 262 263 264 265 266 267 268 269
    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")
270 271
    parser_config.add_argument("--vault", action='store_true',
                               help="Ask for vault password on frontend play")
272
    parser_config.add_argument("name", help="Name of the appliance")
273 274
    parser_config.add_argument("playbook", default="main.yaml", nargs='?',
                               help="Playbook for remote configuration")
275 276 277 278 279 280 281 282 283 284 285 286
    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()