gce 13.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
#!/usr/bin/python
# Copyright 2013 Google Inc.
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.

DOCUMENTATION = '''
---
module: gce
22
version_added: "1.4"
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
short_description: create or terminate GCE instances
description:
     - Creates or terminates Google Compute Engine (GCE) instances.  See
       U(https://cloud.google.com/products/compute-engine) for an overview.
       Full install/configuration instructions for the gce* modules can
       be found in the comments of ansible/test/gce_tests.py.
options:
  image:
    description:
       - image string to use for the instance
    required: false
    default: "debian-7"
    aliases: []
  instance_names:
    description:
      - a comma-separated list of instance names to create or destroy
    required: false
    default: null
    aliases: []
  machine_type:
    description:
      - machine type to use for the instance, use 'n1-standard-1' by default
    required: false
    default: "n1-standard-1"
    aliases: []
  metadata:
    description:
      - a hash/dictionary of custom data for the instance; '{"key":"value",...}'
    required: false
    default: null
    aliases: []
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
  service_account_email:
    version_added: 1.5.1
    description:
      - service account email
    required: false
    default: null
    aliases: []
  pem_file:
    version_added: 1.5.1
    description:
      - path to the pem file associated with the service account email
    required: false
    default: null
    aliases: []
  project_id:
    version_added: 1.5.1
    description:
      - your GCE project ID
    required: false
    default: null
    aliases: []
75 76
  name:
    description:
77
      - identifier when working with a single instance
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
    required: false
    aliases: []
  network:
    description:
      - name of the network, 'default' will be used if not specified
    required: false
    default: "default"
    aliases: []
  persistent_boot_disk:
    description:
      - if set, create the instance with a persistent boot disk
    required: false
    default: "false"
    aliases: []
  state:
    description:
      - desired state of the resource
    required: false
    default: "present"
    choices: ["active", "present", "absent", "deleted"]
    aliases: []
  tags:
    description:
      - a comma-separated list of tags to associate with the instance
    required: false
    default: null
    aliases: []
  zone:
    description:
      - the GCE zone to use
    required: true
    default: "us-central1-a"
    aliases: []

requirements: [ "libcloud" ]
113 114
notes:
  - Either I(name) or I(instance_names) is required.
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
author: Eric Johnson <erjohnso@google.com>
'''

EXAMPLES = '''
# Basic provisioning example.  Create a single Debian 7 instance in the
# us-central1-a Zone of n1-standard-1 machine type.
- local_action:
    module: gce
    name: test-instance
    zone: us-central1-a
    machine_type: n1-standard-1
    image: debian-7

# Example using defaults and with metadata to create a single 'foo' instance
- local_action:
    module: gce
    name: foo
    metadata: '{"db":"postgres", "group":"qa", "id":500}'


# Launch instances from a control node, runs some tasks on the new instances,
# and then terminate them
- name: Create a sandbox instance
  hosts: localhost
  vars:
    names: foo,bar
    machine_type: n1-standard-1
    image: debian-6
    zone: us-central1-a
144 145 146
    service_account_email: unique-email@developer.gserviceaccount.com
    pem_file: /path/to/pem_file
    project_id: project-id
147 148
  tasks:
    - name: Launch instances
149
      local_action: gce instance_names={{names}} machine_type={{machine_type}}
150 151
                    image={{image}} zone={{zone}} service_account_email={{ service_account_email }}
                    pem_file={{ pem_file }} project_id={{ project_id }}
152 153
      register: gce
    - name: Wait for SSH to come up
154
      local_action: wait_for host={{item.public_ip}} port=22 delay=10
155
                    timeout=60 state=started
156
      with_items: {{gce.instance_data}}
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172

- name: Configure instance(s)
  hosts: launched
  sudo: True
  roles:
    - my_awesome_role
    - my_awesome_tasks

- name: Terminate instances
  hosts: localhost
  connection: local
  tasks:
    - name: Terminate instances that were previously launched
      local_action:
        module: gce
        state: 'absent'
173
        instance_names: {{gce.instance_names}}
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203

'''

import sys

try:
    from libcloud.compute.types import Provider
    from libcloud.compute.providers import get_driver
    from libcloud.common.google import GoogleBaseError, QuotaExceededError, \
            ResourceExistsError, ResourceInUseError, ResourceNotFoundError
    _ = Provider.GCE
except ImportError:
    print("failed=True " + \
        "msg='libcloud with GCE support (0.13.3+) required for this module'")
    sys.exit(1)

try:
    from ast import literal_eval
except ImportError:
    print("failed=True " + \
        "msg='GCE module requires python's 'ast' module, python v2.6+'")
    sys.exit(1)


def get_instance_info(inst):
    """Retrieves instance information from an instance object and returns it
    as a dictionary.

    """
    metadata = {}
204
    if 'metadata' in inst.extra and 'items' in inst.extra['metadata']:
205 206 207 208 209 210 211 212 213 214 215 216 217
        for md in inst.extra['metadata']['items']:
            metadata[md['key']] = md['value']

    try:
        netname = inst.extra['networkInterfaces'][0]['network'].split('/')[-1]
    except:
        netname = None
    return({
        'image': not inst.image is None and inst.image.split('/')[-1] or None,
        'machine_type': inst.size,
        'metadata': metadata,
        'name': inst.name,
        'network': netname,
218 219
        'private_ip': inst.private_ips[0],
        'public_ip': inst.public_ips[0],
220 221 222
        'status': ('status' in inst.extra) and inst.extra['status'] or None,
        'tags': ('tags' in inst.extra) and inst.extra['tags'] or [],
        'zone': ('zone' in inst.extra) and inst.extra['zone'].name or None,
223 224 225 226 227 228
    })

def create_instances(module, gce, instance_names):
    """Creates new instances. Attributes other than instance_names are picked
    up from 'module'

229
    module : AnsibleModule object
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265
    gce: authenticated GCE libcloud driver
    instance_names: python list of instance names to create

    Returns:
        A list of dictionaries with instance information
        about the instances that were launched.

    """
    image = module.params.get('image')
    machine_type = module.params.get('machine_type')
    metadata = module.params.get('metadata')
    network = module.params.get('network')
    persistent_boot_disk = module.params.get('persistent_boot_disk')
    state = module.params.get('state')
    tags = module.params.get('tags')
    zone = module.params.get('zone')

    new_instances = []
    changed = False

    lc_image = gce.ex_get_image(image)
    lc_network = gce.ex_get_network(network)
    lc_machine_type = gce.ex_get_size(machine_type)
    lc_zone = gce.ex_get_zone(zone)

    # Try to convert the user's metadata value into the format expected
    # by GCE.  First try to ensure user has proper quoting of a
    # dictionary-like syntax using 'literal_eval', then convert the python
    # dict into a python list of 'key' / 'value' dicts.  Should end up
    # with:
    # [ {'key': key1, 'value': value1}, {'key': key2, 'value': value2}, ...]
    if metadata:
        try:
            md = literal_eval(metadata)
            if not isinstance(md, dict):
                raise ValueError('metadata must be a dict')
266
        except ValueError, e:
267 268
            print("failed=True msg='bad metadata: %s'" % str(e))
            sys.exit(1)
269
        except SyntaxError, e:
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
            print("failed=True msg='bad metadata syntax'")
            sys.exit(1)

        items = []
        for k,v in md.items():
            items.append({"key": k,"value": v})
        metadata = {'items': items}

    # These variables all have default values but check just in case
    if not lc_image or not lc_network or not lc_machine_type or not lc_zone:
        module.fail_json(msg='Missing required create instance variable',
                changed=False)

    for name in instance_names:
        pd = None
        if persistent_boot_disk:
            try:
                pd = gce.create_volume(None, "%s" % name, image=lc_image)
            except ResourceExistsError:
                pd = gce.ex_get_volume("%s" % name, lc_zone)
        inst = None
        try:
            inst = gce.create_node(name, lc_machine_type, lc_image,
                    location=lc_zone, ex_network=network, ex_tags=tags,
                    ex_metadata=metadata, ex_boot_disk=pd)
            changed = True
        except ResourceExistsError:
            inst = gce.ex_get_node(name, lc_zone)
298
        except GoogleBaseError, e:
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
            module.fail_json(msg='Unexpected error attempting to create ' + \
                    'instance %s, error: %s' % (name, e.value))

        if inst:
            new_instances.append(inst)

    instance_names = []
    instance_json_data = []
    for inst in new_instances:
        d = get_instance_info(inst)
        instance_names.append(d['name'])
        instance_json_data.append(d)

    return (changed, instance_json_data, instance_names)


def terminate_instances(module, gce, instance_names, zone_name):
    """Terminates a list of instances.

    module: Ansible module object
    gce: authenticated GCE connection object
    instance_names: a list of instance names to terminate
    zone_name: the zone where the instances reside prior to termination

    Returns a dictionary of instance names that were terminated.

    """
    changed = False
    terminated_instance_names = []
    for name in instance_names:
        inst = None
        try:
            inst = gce.ex_get_node(name, zone_name)
        except ResourceNotFoundError:
            pass
334
        except Exception, e:
335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
            module.fail_json(msg=unexpected_error_msg(e), changed=False)
        if inst:
            gce.destroy_node(inst)
            terminated_instance_names.append(inst.name)
            changed = True

    return (changed, terminated_instance_names)


def main():
    module = AnsibleModule(
        argument_spec = dict(
            image = dict(default='debian-7'),
            instance_names = dict(),
            machine_type = dict(default='n1-standard-1'),
            metadata = dict(),
351
            name = dict(),
352
            network = dict(default='default'),
353
            persistent_boot_disk = dict(type='bool', default=False),
354 355 356
            state = dict(choices=['active', 'present', 'absent', 'deleted'],
                    default='present'),
            tags = dict(type='list'),
357
            zone = dict(default='us-central1-a'),
358 359 360
            service_account_email = dict(),
            pem_file = dict(),
            project_id = dict(),
361 362 363
        )
    )

364 365
    gce = gce_connect(module)

366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418
    image = module.params.get('image')
    instance_names = module.params.get('instance_names')
    machine_type = module.params.get('machine_type')
    metadata = module.params.get('metadata')
    name = module.params.get('name')
    network = module.params.get('network')
    persistent_boot_disk = module.params.get('persistent_boot_disk')
    state = module.params.get('state')
    tags = module.params.get('tags')
    zone = module.params.get('zone')
    changed = False

    inames = []
    if isinstance(instance_names, list):
        inames = instance_names
    elif isinstance(instance_names, str):
        inames = instance_names.split(',')
    if name:
        inames.append(name)
    if not inames:
        module.fail_json(msg='Must specify a "name" or "instance_names"',
                changed=False)
    if not zone:
        module.fail_json(msg='Must specify a "zone"', changed=False)

    json_output = {'zone': zone}
    if state in ['absent', 'deleted']:
        json_output['state'] = 'absent'
        (changed, terminated_instance_names) = terminate_instances(module,
                gce, inames, zone)

        # based on what user specified, return the same variable, although
        # value could be different if an instance could not be destroyed
        if instance_names:
            json_output['instance_names'] = terminated_instance_names
        elif name:
            json_output['name'] = name

    elif state in ['active', 'present']:
        json_output['state'] = 'present'
        (changed, instance_data,instance_name_list) = create_instances(
                module, gce, inames)
        json_output['instance_data'] = instance_data
        if instance_names:
            json_output['instance_names'] = instance_name_list
        elif name:
            json_output['name'] = name


    json_output['changed'] = changed
    print json.dumps(json_output)
    sys.exit(0)

419
# import module snippets
420
from ansible.module_utils.basic import *
421
from ansible.module_utils.gce import *
422 423

main()