From ab47f575dd9446332df51037a6492306f0fc82f3 Mon Sep 17 00:00:00 2001 From: Eric Johnson Date: Mon, 7 Oct 2013 21:01:37 +0000 Subject: [PATCH 1/4] adding Google Compute Engine modules and inventory plugin --- cloud/gce | 417 ++++++++++++++++++++++++++++++++++++++++++++++++++ cloud/gce_lb | 325 +++++++++++++++++++++++++++++++++++++++ cloud/gce_net | 267 ++++++++++++++++++++++++++++++++ cloud/gce_pd | 248 ++++++++++++++++++++++++++++++ 4 files changed, 1257 insertions(+) create mode 100644 cloud/gce create mode 100644 cloud/gce_lb create mode 100644 cloud/gce_net create mode 100644 cloud/gce_pd diff --git a/cloud/gce b/cloud/gce new file mode 100644 index 00000000000..e9d6b28ac9a --- /dev/null +++ b/cloud/gce @@ -0,0 +1,417 @@ +#!/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 . + +DOCUMENTATION = ''' +--- +module: gce +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: [] + name: + description: + - instance name (or name prefix) to be used for each created instance + required: false + default: "gce" + 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" + choices: ["us-central1-a", "us-central1-b", "us-central2-a", "europe-west1-a", "europe-west1-b"] + aliases: [] + +requirements: [ "libcloud" ] +author: Eric Johnson +''' + +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 + tasks: + - name: Launch instances + local_action: gce instance_names=${names} machine_type=${machine_type} + image=${image} zone=${zone} + register: gce + - name: Wait for SSH to come up + local_action: wait_for host=${item.public_ip} port=22 delay=10 + timeout=60 state=started + with_items: ${gce.instance_data} + +- 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' + instance_names: ${gce.instance_names} + +''' + +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) + +# Load in the libcloud secrets file +try: + import secrets +except ImportError: + secrets = None +ARGS = getattr(secrets, 'GCE_PARAMS', ()) +KWARGS = getattr(secrets, 'GCE_KEYWORD_PARAMS', {}) + +if not ARGS or not KWARGS.has_key('project'): + print("failed=True " + \ + "msg='Missing GCE connection parametres in libcloud secrets file.'") + sys.exit(1) + +def unexpected_error_msg(error): + """Create an error string based on passed in error.""" + msg='Unexpected response: HTTP return_code[' + msg+='%s], API error code[%s] and message: %s' % ( + error.http_code, error.code, str(error.value)) + return msg + +def get_instance_info(inst): + """Retrieves instance information from an instance object and returns it + as a dictionary. + + """ + metadata = {} + if inst.extra.has_key('metadata') and inst.extra['metadata'].has_key('items'): + 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, + 'private_ip': inst.private_ip[0], + 'public_ip': inst.public_ip[0], + 'status': inst.extra.has_key('status') and inst.extra['status'] or None, + 'tags': inst.extra.has_key('tags') and inst.extra['tags'] or [], + 'zone': inst.extra.has_key('zone') and inst.extra['zone'].name or None, + }) + +def create_instances(module, gce, instance_names): + """Creates new instances. Attributes other than instance_names are picked + up from 'module' + + module : AnsbileModule object + 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') + except ValueError as e: + print("failed=True msg='bad metadata: %s'" % str(e)) + sys.exit(1) + except SyntaxError as e: + 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) + except GoogleBaseError as e: + 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 + except Exception as e: + 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(), + name = dict(), + network = dict(default='default'), + persistent_boot_disk = dict(choices=BOOLEANS, default=False), + state = dict(choices=['active', 'present', 'absent', 'deleted'], + default='present'), + tags = dict(type='list'), + zone = dict(choices=['us-central1-a', 'us-central1-b', + 'us-central2-a', 'europe-west1-a', 'europe-west1-b'], + default='us-central1-a'), + ) + ) + + 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 + + try: + gce = get_driver(Provider.GCE)(*ARGS, datacenter=zone, **KWARGS) + except Exception as e: + module.fail_json(msg=unexpected_error_msg(e), 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) + +# this is magic, see lib/ansible/module_common.py +#<> + +main() diff --git a/cloud/gce_lb b/cloud/gce_lb new file mode 100644 index 00000000000..fbead066f4f --- /dev/null +++ b/cloud/gce_lb @@ -0,0 +1,325 @@ +#!/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 . + +DOCUMENTATION = ''' +--- +module: gce_lb +short_description: create/destroy GCE load-balancer resources +description: + - This module can create and destroy Google Compue Engine C(loadbalancer) + and C(httphealthcheck) resources. The primary LB resource is the + C(load_balancer) resource and the health check parameters are all + prefixed with I(httphealthcheck). + The full documentation for Google Compute Engine load balancing is at + U(https://developers.google.com/compute/docs/load-balancing/). However, + the ansible module simplifies the configuration by following the + libcloud model. + Full install/configuration instructions for the gce* modules can + be found in the comments of ansible/test/gce_tests.py. +options: + httphealthcheck_name: + description: + - the name identifier for the HTTP health check + required: false + default: null + httphealthcheck_port: + description: + - the TCP port to use for HTTP health checking + required: false + default: 80 + httphealthcheck_path: + description: + - the url path to use for HTTP health checking + required: false + default: "/" + httphealthcheck_interval: + description: + - the duration in seconds between each health check request + required: false + default: 5 + httphealthcheck_timeout: + description: + - the timeout in seconds before a request is considered a failed check + required: false + default: 5 + httphealthcheck_unhealthy_count: + description: + - number of consecutive failed checks before marking a node unhealthy + required: false + default: 2 + httphealthcheck_healthy_count: + description: + - number of consecutive successful checks before marking a node healthy + required: false + default: 2 + httphealthcheck_host: + description: + - host header to pass through on HTTP check requests + required: false + default: null + name: + description: + - name of the load-balancer resource + required: false + default: null + protocol: + description: + - the protocol used for the load-balancer packet forwarding, tcp or udp + required: false + default: "tcp" + choices: ['tcp', 'udp'] + region: + description: + - the GCE region where the load-balancer is defined + required: false + choices: ["us-central1", "us-central2", "europe-west1"] + external_ip: + description: + - the external static IPv4 (or auto-assigned) address for the LB + required: false + default: null + port_range: + description: + - the port (range) to forward, e.g. 80 or 8000-8888 defaults to all ports + required: false + default: null + members: + description: + - a list of zone/nodename pairs, e.g ['us-central1-a/www-a', ...] + required: false + aliases: ['nodes'] + state: + description: + - desired state of the LB + default: "present" + choices: ["active", "present", "absent", "deleted"] + aliases: [] + +requirements: [ "libcloud" ] +author: Eric Johnson +''' + +EXAMPLES = ''' +# Simple example of creating a new LB, adding members, and a health check +- local_action: + module: gce_lb + name: testlb + region: us-central1 + members: ["us-central1-a/www-a", "us-central1-b/www-b"] + httphealthcheck_name: hc + httphealthcheck_port: 80 + httphealthcheck_path: "/up" +''' + +import sys + +try: + from libcloud.compute.types import Provider + from libcloud.compute.providers import get_driver + from libcloud.loadbalancer.types import Provider as Provider_lb + from libcloud.loadbalancer.providers import get_driver as get_driver_lb + from libcloud.common.google import GoogleBaseError, QuotaExceededError, \ + ResourceExistsError, ResourceNotFoundError + _ = Provider.GCE +except ImportError: + print("failed=True " + \ + "msg='libcloud with GCE support required for this module.'") + sys.exit(1) + +# Load in the libcloud secrets file +try: + import secrets +except ImportError: + secrets = None +ARGS = getattr(secrets, 'GCE_PARAMS', ()) +KWARGS = getattr(secrets, 'GCE_KEYWORD_PARAMS', {}) + +if not ARGS or not KWARGS.has_key('project'): + print("failed=True msg='Missing GCE connection " + \ + "parameters in libcloud secrets file.'") + sys.exit(1) + +def unexpected_error_msg(error): + """Format error string based on passed in error.""" + msg='Unexpected response: HTTP return_code[' + msg+='%s], API error code[%s] and message: %s' % ( + error.http_code, error.code, str(error.value)) + return msg + +def main(): + module = AnsibleModule( + argument_spec = dict( + httphealthcheck_name = dict(), + httphealthcheck_port = dict(default=80), + httphealthcheck_path = dict(default='/'), + httphealthcheck_interval = dict(default=5), + httphealthcheck_timeout = dict(default=5), + httphealthcheck_unhealthy_count = dict(default=2), + httphealthcheck_healthy_count = dict(default=2), + httphealthcheck_host = dict(), + name = dict(), + protocol = dict(default='tcp'), + region = dict(), + external_ip = dict(), + port_range = dict(), + members = dict(type='list'), + state = dict(default='present'), + ) + ) + + httphealthcheck_name = module.params.get('httphealthcheck_name') + httphealthcheck_port = module.params.get('httphealthcheck_port') + httphealthcheck_path = module.params.get('httphealthcheck_path') + httphealthcheck_interval = module.params.get('httphealthcheck_interval') + httphealthcheck_timeout = module.params.get('httphealthcheck_timeout') + httphealthcheck_unhealthy_count = \ + module.params.get('httphealthcheck_unhealthy_count') + httphealthcheck_healthy_count = \ + module.params.get('httphealthcheck_healthy_count') + httphealthcheck_host = module.params.get('httphealthcheck_host') + name = module.params.get('name') + protocol = module.params.get('protocol') + region = module.params.get('region') + external_ip = module.params.get('external_ip') + port_range = module.params.get('port_range') + members = module.params.get('members') + state = module.params.get('state') + + try: + gce = get_driver(Provider.GCE)(*ARGS, **KWARGS) + gcelb = get_driver_lb(Provider_lb.GCE)(gce_driver=gce) + except Exception as e: + module.fail_json(msg=unexpected_error_msg(e), changed=False) + + changed = False + json_output = {'name': name, 'state': state} + + if not name and not httphealthcheck_name: + module.fail_json(msg='Nothing to do, please specify a "name" ' + \ + 'or "httphealthcheck_name" parameter', changed=False) + + if state in ['active', 'present']: + # first, create the httphealthcheck if requested + hc = None + if httphealthcheck_name: + json_output['httphealthcheck_name'] = httphealthcheck_name + try: + hc = gcelb.ex_create_healthcheck(httphealthcheck_name, + host=httphealthcheck_host, path=httphealthcheck_path, + port=httphealthcheck_port, + interval=httphealthcheck_interval, + timeout=httphealthcheck_timeout, + unhealthy_threshold=httphealthcheck_unhealthy_count, + healthy_threshold=httphealthcheck_healthy_count) + changed = True + except ResourceExistsError: + hc = gce.ex_get_healthcheck(httphealthcheck_name) + except Exception as e: + module.fail_json(msg=unexpected_error_msg(e), changed=False) + + if hc is not None: + json_output['httphealthcheck_host'] = hc.extra['host'] + json_output['httphealthcheck_path'] = hc.path + json_output['httphealthcheck_port'] = hc.port + json_output['httphealthcheck_interval'] = hc.interval + json_output['httphealthcheck_timeout'] = hc.timeout + json_output['httphealthcheck_unhealthy_count'] = \ + hc.unhealthy_threshold + json_output['httphealthcheck_healthy_count'] = \ + hc.healthy_threshold + + # create the forwarding rule (and target pool under the hood) + lb = None + if name: + if not region: + module.fail_json(msg='Missing required region name', + changed=False) + nodes = [] + output_nodes = [] + json_output['name'] = name + # members is a python list of 'zone/inst' strings + if members: + for node in members: + try: + zone, node_name = node.split('/') + nodes.append(gce.ex_get_node(node_name, zone)) + output_nodes.append(node) + except: + # skip nodes that are badly formatted or don't exist + pass + try: + if hc is not None: + lb = gcelb.create_balancer(name, port_range, protocol, + None, nodes, ex_region=region, ex_healthchecks=[hc], + ex_address=external_ip) + else: + lb = gcelb.create_balancer(name, port_range, protocol, + None, nodes, ex_region=region, ex_address=external_ip) + changed = True + except ResourceExistsError: + lb = gcelb.get_balancer(name) + except Exception as e: + module.fail_json(msg=unexpected_error_msg(e), changed=False) + + if lb is not None: + json_output['members'] = output_nodes + json_output['protocol'] = protocol + json_output['region'] = region + json_output['external_ip'] = lb.ip + json_output['port_range'] = lb.port + hc_names = [] + if lb.extra.has_key('healthchecks'): + for hc in lb.extra['healthchecks']: + hc_names.append(hc.name) + json_output['httphealthchecks'] = hc_names + + if state in ['absent', 'deleted']: + # first, delete the load balancer (forwarding rule and target pool) + # if specified. + if name: + json_output['name'] = name + try: + lb = gcelb.get_balancer(name) + gcelb.destroy_balancer(lb) + changed = True + except ResourceNotFoundError: + pass + except Exception as e: + module.fail_json(msg=unexpected_error_msg(e), changed=False) + + # destroy the health check if specified + if httphealthcheck_name: + json_output['httphealthcheck_name'] = httphealthcheck_name + try: + hc = gce.ex_get_healthcheck(httphealthcheck_name) + gce.ex_destroy_healthcheck(hc) + changed = True + except ResourceNotFoundError: + pass + except Exception as e: + module.fail_json(msg=unexpected_error_msg(e), changed=False) + + + json_output['changed'] = changed + print json.dumps(json_output) + sys.exit(0) + +# this is magic, see lib/ansible/module_common.py +#<> + +main() diff --git a/cloud/gce_net b/cloud/gce_net new file mode 100644 index 00000000000..903d2b23e6a --- /dev/null +++ b/cloud/gce_net @@ -0,0 +1,267 @@ +#!/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 . + +DOCUMENTATION = ''' +--- +module: gce_net +short_description: create/destroy GCE networks and firewall rules +description: + - This module can create and destroy Google Compue Engine networks and + firewall rules U(https://developers.google.com/compute/docs/networking). + The I(name) parameter is reserved for referencing a network while the + I(fwname) parameter is used to reference firewall rules. + IPv4 Address ranges must be specified using the CIDR + U(http://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) format. + Full install/configuration instructions for the gce* modules can + be found in the comments of ansible/test/gce_tests.py. +options: + allowed: + description: + - the protocol:ports to allow ('tcp:80' or 'tcp:80,443' or 'tcp:80-800') + required: false + default: null + aliases: [] + ipv4_range: + description: + - the IPv4 address range in CIDR notation for the network + required: false + aliases: ['cidr'] + fwname: + description: + - name of the firewall rule + required: false + default: null + aliases: ['fwrule'] + name: + description: + - name of the network + required: false + default: null + aliases: [] + src_range: + description: + - the source IPv4 address range in CIDR notation + required: false + default: null + aliases: ['src_cidr'] + src_tags: + description: + - the source instance tags for creating a firewall rule + required: false + default: null + aliases: [] + state: + description: + - desired state of the persistent disk + required: false + default: "present" + choices: ["active", "present", "absent", "deleted"] + aliases: [] + +requirements: [ "libcloud" ] +author: Eric Johnson +''' + +EXAMPLES = ''' +# Simple example of creating a new network +- local_action: + module: gce_net + name: privatenet + ipv4_range: '10.240.16.0/24' + +# Simple example of creating a new firewall rule +- local_action: + module: gce_net + name: privatenet + allowed: tcp:80,8080 + src_tags: ["web", "proxy"] + +''' + +import sys + +try: + from libcloud.compute.types import Provider + from libcloud.compute.providers import get_driver + from libcloud.common.google import GoogleBaseError, QuotaExceededError, \ + ResourceExistsError, ResourceNotFoundError + _ = Provider.GCE +except ImportError: + print("failed=True " + \ + "msg='libcloud with GCE support required for this module.'") + sys.exit(1) + +# Load in the libcloud secrets file +try: + import secrets +except ImportError: + secrets = None +ARGS = getattr(secrets, 'GCE_PARAMS', ()) +KWARGS = getattr(secrets, 'GCE_KEYWORD_PARAMS', {}) + +if not ARGS or not KWARGS.has_key('project'): + print("failed=True msg='Missing GCE connection " + \ + "parameters in libcloud secrets file.'") + sys.exit(1) + +def unexpected_error_msg(error): + """Format error string based on passed in error.""" + msg='Unexpected response: HTTP return_code[' + msg+='%s], API error code[%s] and message: %s' % ( + error.http_code, error.code, str(error.value)) + return msg + +def format_allowed(allowed): + """Format the 'allowed' value so that it is GCE compatible.""" + if allowed.count(":") == 0: + protocol = allowed + ports = [] + elif allowed.count(":") == 1: + protocol, ports = allowed.split(":") + else: + return [] + if ports.count(","): + ports = ports.split(",") + else: + ports = [ports] + return_val = {"IPProtocol": protocol} + if ports: + return_val["ports"] = ports + return [return_val] + + +def main(): + module = AnsibleModule( + argument_spec = dict( + allowed = dict(), + ipv4_range = dict(), + fwname = dict(), + name = dict(), + src_range = dict(), + src_tags = dict(type='list'), + state = dict(default='present'), + ) + ) + + allowed = module.params.get('allowed') + ipv4_range = module.params.get('ipv4_range') + fwname = module.params.get('fwname') + name = module.params.get('name') + src_range = module.params.get('src_range') + src_tags = module.params.get('src_tags') + state = module.params.get('state') + + try: + gce = get_driver(Provider.GCE)(*ARGS, **KWARGS) + except Exception as e: + module.fail_json(msg=unexpected_error_msg(e), changed=False) + + changed = False + json_output = {'state': state} + + if state in ['active', 'present']: + network = None + try: + network = gce.ex_get_network(name) + json_output['name'] = name + json_output['ipv4_range'] = network.cidr + except ResourceNotFoundError: + pass + except Exception as e: + module.fail_json(msg=unexpected_error_msg(e), changed=False) + + # user wants to create a new network that doesn't yet exist + if name and not network: + if not ipv4_range: + module.fail_json(msg="Missing required 'ipv4_range' parameter", + changed=False) + + try: + network = gce.ex_create_network(name, ipv4_range) + json_output['name'] = name + json_output['ipv4_range'] = ipv4_range + changed = True + except Exception as e: + module.fail_json(msg=unexpected_error_msg(e), changed=False) + + if fwname: + # user creating a firewall rule + if not allowed and not src_range and not src_tags: + if changed and network: + module.fail_json( + msg="Network created, but missing required " + \ + "firewall rule parameter(s)", changed=True) + module.fail_json( + msg="Missing required firewall rule parameter(s)", + changed=False) + + allowed_list = format_allowed(allowed) + + try: + gce.ex_create_firewall(fwname, allowed_list, network=name, + source_ranges=src_range, source_tags=src_tags) + changed = True + except ResourceExistsError: + pass + except Exception as e: + module.fail_json(msg=unexpected_error_msg(e), changed=False) + + json_output['fwname'] = fwname + json_output['allowed'] = allowed + json_output['src_range'] = src_range + json_output['src_tags'] = src_tags + + if state in ['absent', 'deleted']: + if fwname: + json_output['fwname'] = fwname + fw = None + try: + fw = gce.ex_get_firewall(fwname) + except ResourceNotFoundError: + pass + except Exception as e: + module.fail_json(msg=unexpected_error_msg(e), changed=False) + if fw: + gce.ex_destroy_firewall(fw) + changed = True + if name: + json_output['name'] = name + network = None + try: + network = gce.ex_get_network(name) +# json_output['d1'] = 'found network name %s' % name + except ResourceNotFoundError: +# json_output['d2'] = 'not found network name %s' % name + pass + except Exception as e: +# json_output['d3'] = 'error with %s' % name + module.fail_json(msg=unexpected_error_msg(e), changed=False) + if network: +# json_output['d4'] = 'deleting %s' % name + gce.ex_destroy_network(network) +# json_output['d5'] = 'deleted %s' % name + changed = True + + json_output['changed'] = changed + print json.dumps(json_output) + sys.exit(0) + +# this is magic, see lib/ansible/module_common.py +#<> + +main() diff --git a/cloud/gce_pd b/cloud/gce_pd new file mode 100644 index 00000000000..36d4750a5d5 --- /dev/null +++ b/cloud/gce_pd @@ -0,0 +1,248 @@ +#!/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 . + +DOCUMENTATION = ''' +--- +module: gce_pd +short_description: utilize GCE persistent disk resources +description: + - This module can create and destroy unformatted GCE persistent disks + U(https://developers.google.com/compute/docs/disks#persistentdisks). + It also supports attaching and detaching disks from running instances + but does not support creating boot disks from images or snapshots. The + 'gce' module supports creating instances with boot disks. + Full install/configuration instructions for the gce* modules can + be found in the comments of ansible/test/gce_tests.py. +options: + detach_only: + description: + - do not destroy the disk, merely detach it from an instance + required: false + default: "no" + choices: ["yes", "no"] + aliases: [] + instance_name: + description: + - instance name if you wish to attach or detach the disk + required: false + default: null + aliases: [] + mode: + description: + - GCE mount mode of disk, READ_ONLY (default) or READ_WRITE + required: false + default: "READ_ONLY" + choices: ["READ_WRITE", "READ_ONLY"] + aliases: [] + name: + description: + - name of the disk + required: true + default: null + aliases: [] + size_gb: + description: + - whole integer size of disk (in GB) to create, default is 10 GB + required: false + default: 10 + aliases: [] + state: + description: + - desired state of the persistent disk + required: false + default: "present" + choices: ["active", "present", "absent", "deleted"] + aliases: [] + zone: + description: + - zone in which to create the disk + required: false + default: "us-central1-b" + aliases: [] + +requirements: [ "libcloud" ] +author: Eric Johnson +''' + +EXAMPLES = ''' +# Simple attachment action to an existing instance +- local_action: + module: gce_pd + instance_name: notlocalhost + size_gb: 5 + name: pd +''' + +import sys + +try: + from libcloud.compute.types import Provider + from libcloud.compute.providers import get_driver + from libcloud.common.google import GoogleBaseError, QuotaExceededError, \ + ResourceExistsError, ResourceNotFoundError, ResourceInUseError + _ = Provider.GCE +except ImportError: + print("failed=True " + \ + "msg='libcloud with GCE support is required for this module.'") + sys.exit(1) + +# Load in the libcloud secrets file +try: + import secrets +except ImportError: + secrets = None +ARGS = getattr(secrets, 'GCE_PARAMS', ()) +KWARGS = getattr(secrets, 'GCE_KEYWORD_PARAMS', {}) + +if not ARGS or not KWARGS.has_key('project'): + print("failed=True " + \ + "msg='Missing GCE connection parameters in libcloud secrets file.'") + sys.exit(1) + +def unexpected_error_msg(error): + msg='Unexpected response: HTTP return_code[' + msg+='%s], API error code[%s] and message: %s' % ( + error.http_code, error.code, str(error.value)) + return msg + +def main(): + module = AnsibleModule( + argument_spec = dict( + detach_only = dict(choice=BOOLEANS), + instance_name = dict(), + mode = dict(default='READ_ONLY', + choices=['READ_WRITE', 'READ_ONLY']), + name = dict(required=True), + size_gb = dict(default=10), + state = dict(default='present'), + zone = dict(default='us-central1-b'), + ) + ) + + detach_only = module.params.get('detach_only') + instance_name = module.params.get('instance_name') + mode = module.params.get('mode') + name = module.params.get('name') + size_gb = module.params.get('size_gb') + state = module.params.get('state') + zone = module.params.get('zone') + + if detach_only and not instance_name: + module.fail_json( + msg='Must specify an instance name when detaching a disk', + changed=False) + + try: + gce = get_driver(Provider.GCE)(*ARGS, datacenter=zone, **KWARGS) + except Exception as e: + module.fail_json(msg=unexpected_error_msg(e), changed=False) + + disk = inst = None + changed = is_attached = False + + json_output = { 'name': name, 'zone': zone, 'state': state } + if detach_only: + json_output['detach_only'] = True + json_output['detached_from_instance'] = instance_name + + if instance_name: + # user wants to attach/detach from an existing instance + try: + inst = gce.ex_get_node(instance_name, zone) + # is the disk attached? + for d in inst.extra['disks']: + if d['deviceName'] == name: + is_attached = True + json_output['attached_mode'] = d['mode'] + json_output['attached_to_instance'] = inst.name + except: + pass + + # find disk if it already exists + try: + disk = gce.ex_get_volume(name) + json_output['size_gb'] = int(disk.size) + except ResourceNotFoundError: + pass + except Exception as e: + module.fail_json(msg=unexpected_error_msg(e), changed=False) + + # user wants a disk to exist. If "instance_name" is supplied the user + # also wants it attached + if state in ['active', 'present']: + + if not size_gb: + module.fail_json(msg="Must supply a size_gb", changed=False) + try: + size_gb = int(round(float(size_gb))) + if size_gb < 1: raise Exception + except: + module.fail_json(msg="Must supply a size_gb larger than 1 GB", + changed=False) + + if instance_name and inst is None: + module.fail_json(msg='Instance %s does not exist in zone %s' % ( + instance_name, zone), changed=False) + + if not disk: + try: + disk = gce.create_volume(size_gb, name, location=zone) + except ResourceExistsError: + pass + except QuotaExceededError: + module.fail_json(msg='Requested disk size exceeds quota', + changed=False) + except Exception as e: + module.fail_json(msg=unexpected_error_msg(e), changed=False) + json_output['size_gb'] = size_gb + changed = True + if inst and not is_attached: + try: + gce.attach_volume(inst, disk, device=name, ex_mode=mode) + except Exception as e: + module.fail_json(msg=unexpected_error_msg(e), changed=False) + json_output['attached_to_instance'] = inst.name + json_output['attached_mode'] = mode + changed = True + + # user wants to delete a disk (or perhaps just detach it). + if state in ['absent', 'deleted'] and disk: + + if inst and is_attached: + try: + gce.detach_volume(disk, ex_node=inst) + except Exception as e: + module.fail_json(msg=unexpected_error_msg(e), changed=False) + changed = True + if not detach_only: + try: + gce.destroy_volume(disk) + except ResourceInUseError as e: + module.fail_json(msg=str(e.value), changed=False) + except Exception as e: + module.fail_json(msg=unexpected_error_msg(e), changed=False) + changed = True + + json_output['changed'] = changed + print json.dumps(json_output) + sys.exit(0) + +# this is magic, see lib/ansible/module_common.py +#<> + +main() From ea8147802a9f89ed9f1aa6c7ae6604a38df2a86d Mon Sep 17 00:00:00 2001 From: Eric Johnson Date: Tue, 8 Oct 2013 16:36:35 +0000 Subject: [PATCH 2/4] custom user-agent header --- cloud/gce | 5 +++++ cloud/gce_lb | 7 +++++++ cloud/gce_net | 5 +++++ cloud/gce_pd | 5 +++++ 4 files changed, 22 insertions(+) diff --git a/cloud/gce b/cloud/gce index e9d6b28ac9a..7f13751f813 100644 --- a/cloud/gce +++ b/cloud/gce @@ -150,6 +150,9 @@ EXAMPLES = ''' import sys +USER_AGENT_PRODUCT="Ansible-gce" +USER_AGENT_VERSION="v1beta15" + try: from libcloud.compute.types import Provider from libcloud.compute.providers import get_driver @@ -367,6 +370,8 @@ def main(): try: gce = get_driver(Provider.GCE)(*ARGS, datacenter=zone, **KWARGS) + gce.connection.user_agent_append("%s/%s" % ( + USER_AGENT_PRODUCT, USER_AGENT_VERSION)) except Exception as e: module.fail_json(msg=unexpected_error_msg(e), changed=False) diff --git a/cloud/gce_lb b/cloud/gce_lb index fbead066f4f..d5205ff30ed 100644 --- a/cloud/gce_lb +++ b/cloud/gce_lb @@ -128,6 +128,9 @@ EXAMPLES = ''' import sys +USER_AGENT_PRODUCT="Ansible-gce_lb" +USER_AGENT_VERSION="v1beta15" + try: from libcloud.compute.types import Provider from libcloud.compute.providers import get_driver @@ -202,7 +205,11 @@ def main(): try: gce = get_driver(Provider.GCE)(*ARGS, **KWARGS) + gce.connection.user_agent_append("%s/%s" % ( + USER_AGENT_PRODUCT, USER_AGENT_VERSION)) gcelb = get_driver_lb(Provider_lb.GCE)(gce_driver=gce) + gcelb.connection.user_agent_append("%s/%s" % ( + USER_AGENT_PRODUCT, USER_AGENT_VERSION)) except Exception as e: module.fail_json(msg=unexpected_error_msg(e), changed=False) diff --git a/cloud/gce_net b/cloud/gce_net index 903d2b23e6a..65f2d022a92 100644 --- a/cloud/gce_net +++ b/cloud/gce_net @@ -95,6 +95,9 @@ EXAMPLES = ''' import sys +USER_AGENT_PRODUCT="Ansible-gce_net" +USER_AGENT_VERSION="v1beta15" + try: from libcloud.compute.types import Provider from libcloud.compute.providers import get_driver @@ -168,6 +171,8 @@ def main(): try: gce = get_driver(Provider.GCE)(*ARGS, **KWARGS) + gce.connection.user_agent_append("%s/%s" % ( + USER_AGENT_PRODUCT, USER_AGENT_VERSION)) except Exception as e: module.fail_json(msg=unexpected_error_msg(e), changed=False) diff --git a/cloud/gce_pd b/cloud/gce_pd index 36d4750a5d5..b60d4fe9bb5 100644 --- a/cloud/gce_pd +++ b/cloud/gce_pd @@ -90,6 +90,9 @@ EXAMPLES = ''' import sys +USER_AGENT_PRODUCT="Ansible-gce_pd" +USER_AGENT_VERSION="v1beta15" + try: from libcloud.compute.types import Provider from libcloud.compute.providers import get_driver @@ -149,6 +152,8 @@ def main(): try: gce = get_driver(Provider.GCE)(*ARGS, datacenter=zone, **KWARGS) + gce.connection.user_agent_append("%s/%s" % ( + USER_AGENT_PRODUCT, USER_AGENT_VERSION)) except Exception as e: module.fail_json(msg=unexpected_error_msg(e), changed=False) From b4533c880b7e0de13b51592c7f43ea092ff5cd55 Mon Sep 17 00:00:00 2001 From: Eric Johnson Date: Sun, 13 Oct 2013 13:41:55 -0700 Subject: [PATCH 3/4] address a few review comments --- cloud/gce | 4 ++-- cloud/gce_lb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/gce b/cloud/gce index 7f13751f813..5b66bebad7a 100644 --- a/cloud/gce +++ b/cloud/gce @@ -222,7 +222,7 @@ def create_instances(module, gce, instance_names): """Creates new instances. Attributes other than instance_names are picked up from 'module' - module : AnsbileModule object + module : AnsibleModule object gce: authenticated GCE libcloud driver instance_names: python list of instance names to create @@ -346,7 +346,7 @@ def main(): metadata = dict(), name = dict(), network = dict(default='default'), - persistent_boot_disk = dict(choices=BOOLEANS, default=False), + persistent_boot_disk = dict(type='bool', choices=BOOLEANS, default=False), state = dict(choices=['active', 'present', 'absent', 'deleted'], default='present'), tags = dict(type='list'), diff --git a/cloud/gce_lb b/cloud/gce_lb index d5205ff30ed..97e82fce19d 100644 --- a/cloud/gce_lb +++ b/cloud/gce_lb @@ -21,7 +21,7 @@ DOCUMENTATION = ''' module: gce_lb short_description: create/destroy GCE load-balancer resources description: - - This module can create and destroy Google Compue Engine C(loadbalancer) + - This module can create and destroy Google Compute Engine C(loadbalancer) and C(httphealthcheck) resources. The primary LB resource is the C(load_balancer) resource and the health check parameters are all prefixed with I(httphealthcheck). From b2126328081087ad0f9ed93a9aa2e47119d9dde0 Mon Sep 17 00:00:00 2001 From: Eric Johnson Date: Sun, 13 Oct 2013 13:48:09 -0700 Subject: [PATCH 4/4] update defaults and help for 'name' and 'instance_names' --- cloud/gce | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cloud/gce b/cloud/gce index 5b66bebad7a..b8a3a7c0a82 100644 --- a/cloud/gce +++ b/cloud/gce @@ -52,9 +52,8 @@ options: aliases: [] name: description: - - instance name (or name prefix) to be used for each created instance + - identifier when working with a single instance required: false - default: "gce" aliases: [] network: description: @@ -344,7 +343,7 @@ def main(): instance_names = dict(), machine_type = dict(default='n1-standard-1'), metadata = dict(), - name = dict(), + name = dict(default='gce'), network = dict(default='default'), persistent_boot_disk = dict(type='bool', choices=BOOLEANS, default=False), state = dict(choices=['active', 'present', 'absent', 'deleted'],