From 17a4a655a32f0a4a8f258c8ab701a2de4255c55a Mon Sep 17 00:00:00 2001 From: GwenaelPellenArkeup Date: Wed, 1 Mar 2017 22:50:07 +0100 Subject: [PATCH] [cloud] New module: gce_template (#20918) * add gce_template.py gce template manage GCE Instance Templates in Google Cloud Plateform. * change gce_template on style/formating Apply change on style/formating from reviewer request. * change gce_template on style/formating again. * Rename gce_template.py to gce_instance_template.py * update gce_instance_template.py to pass CI Oops :) ERROR: Import found before documentation variables. All imports must appear below DOCUMENTATION/EXAMPLES/RETURN/ANSIBLE_METADATA. * Update gce_instance_template.py change documentation replace gce_template by gce_instance_template * Update gce_instance_template.py Sorry for the typography fault. The day begins badly. * Update gce_instance_template.py apply text change s/Compte/Compute/ s/Plateform/Platform/ s/forword/forward/ change documentation for subnetwork, subnetwork is name. add mutually_exclusive in AnsibleModule arguments. change disk_type as an option type. * Update gce_instance_template.py change the documentation. * Update gce_instance_template.py RETURN is required for all new modules. Is empty because no new return variable returned. * Update gce_instance_template.py Ansible will verify that only present/absent are passed as the state. This else is not needed. --- .../cloud/google/gce_instance_template.py | 562 ++++++++++++++++++ 1 file changed, 562 insertions(+) create mode 100644 lib/ansible/modules/cloud/google/gce_instance_template.py diff --git a/lib/ansible/modules/cloud/google/gce_instance_template.py b/lib/ansible/modules/cloud/google/gce_instance_template.py new file mode 100644 index 00000000000..6508196cbff --- /dev/null +++ b/lib/ansible/modules/cloud/google/gce_instance_template.py @@ -0,0 +1,562 @@ +#!/usr/bin/python +# 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 . + +ANSIBLE_METADATA = { + 'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0' +} + +DOCUMENTATION = ''' +--- +module: gce_instance_template +version_added: "2.3" +short_description: create or destroy intance templates of Compute Engine of GCP. +description: + - Creates or destroy Google instance templates + of Compute Engine of Google Cloud Platform. +options: + state: + description: + - The desired state for the instance template. + default: "present" + choices: ["present", "absent"] + name: + description: + - The name of the GCE instance template. + required: true + default: null + size: + description: + - The desired machine type for the instance template. + default: "f1-micro" + source: + description: + - A source disk to attach to the instance. + Cannot specify both I(image) and I(source). + default: null + image: + description: + - The image to use to create the instance. + Cannot specify both both I(image) and I(source). + default: null + image_family: + description: + - The image family to use to create the instance. + If I(image) has been used I(image_family) is ignored. + Cannot specify both I(image) and I(source). + default: null + disk_type: + description: + - Specify a C(pd-standard) disk or C(pd-ssd) + for an SSD disk. + default: pd-standard + disk_auto_delete: + description: + - Indicate that the boot disk should be + deleted when the Node is deleted. + default: true + network: + description: + - The network to associate with the instance. + default: "default" + subnetwork: + description: + - The Subnetwork resource name for this instance. + default: null + can_ip_forward: + description: + - Set to True to allow instance to + send/receive non-matching src/dst packets. + default: false + external_ip: + description: + - The external IP address to use. + If C(ephemeral), a new non-static address will be + used. If C(None), then no external address will + be used. To use an existing static IP address + specify adress name. + default: "ephemeral" + service_account_email: + description: + - service account email + default: null + service_account_permissions: + description: + - service account permissions (see + U(https://cloud.google.com/sdk/gcloud/reference/compute/instances/create), + --scopes section for detailed information) + default: null + choices: [ + "bigquery", "cloud-platform", "compute-ro", "compute-rw", + "useraccounts-ro", "useraccounts-rw", "datastore", "logging-write", + "monitoring", "sql-admin", "storage-full", "storage-ro", + "storage-rw", "taskqueue", "userinfo-email" + ] + automatic_restart: + description: + - Defines whether the instance should be + automatically restarted when it is + terminated by Compute Engine. + default: null + preemptible: + description: + - Defines whether the instance is preemptible. + default: null + tags: + description: + - a comma-separated list of tags to associate with the instance + default: null + metadata: + description: + - a hash/dictionary of custom data for the instance; + '{"key":"value", ...}' + default: null + description: + description: + - description of instance template + default: null + disks: + description: + - a list of persistent disks to attach to the instance; a string value + gives the name of the disk; alternatively, a dictionary value can + define 'name' and 'mode' ('READ_ONLY' or 'READ_WRITE'). The first entry + will be the boot disk (which must be READ_WRITE). + default: null + nic_gce_struct: + description: + - Support passing in the GCE-specific + formatted networkInterfaces[] structure. + default: null + project_id: + description: + - your GCE project ID + default: null + pem_file: + description: + - path to the pem file associated with the service account email + This option is deprecated. Use 'credentials_file'. + default: null + credentials_file: + description: + - path to the JSON file associated with the service account email + default: null +requirements: + - "python >= 2.6" + - "apache-libcloud >= 0.13.3, >= 0.17.0 if using JSON credentials, + >= 0.20.0 if using preemptible option" +notes: + - JSON credentials strongly preferred. +author: "Gwenael Pellen (@GwenaelPellenArkeup) " +''' + +EXAMPLES = ''' +# Usage +- name: create instance template named foo + gce_instance_template: + name: foo + size: n1-standard-1 + image_family: ubuntu-1604-lts + state: present + project_id: "your-project-name" + credentials_file: "/path/to/your-key.json" + service_account_email: "your-sa@your-project-name.iam.gserviceaccount.com" + +# Example Playbook +- name: Compute Engine Instance Template Examples + hosts: localhost + vars: + service_account_email: "your-sa@your-project-name.iam.gserviceaccount.com" + credentials_file: "/path/to/your-key.json" + project_id: "your-project-name" + tasks: + - name: create instance template + gce_instance_template: + name: my-test-instance-template + size: n1-standard-1 + image_family: ubuntu-1604-lts + state: present + project_id: "{{ project_id }}" + credentials_file: "{{ credentials_file }}" + service_account_email: "{{ service_account_email }}" + - name: delete instance template + gce_instance_template: + name: my-test-instance-template + size: n1-standard-1 + image_family: ubuntu-1604-lts + state: absent + project_id: "{{ project_id }}" + credentials_file: "{{ credentials_file }}" + service_account_email: "{{ service_account_email }}" +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.gce import gce_connect + +try: + import libcloud + from libcloud.compute.types import Provider + from libcloud.compute.providers import get_driver + from libcloud.common.google import GoogleBaseError, QuotaExceededError, \ + ResourceExistsError, ResourceInUseError, ResourceNotFoundError + from libcloud.compute.drivers.gce import GCEAddress + _ = Provider.GCE + HAS_LIBCLOUD = True +except ImportError: + HAS_LIBCLOUD = False + +try: + from ast import literal_eval + HAS_PYTHON26 = True +except ImportError: + HAS_PYTHON26 = False + + +def get_info(inst): + """Retrieves instance template information + + """ + return({ + 'name': inst.name, + 'extra': inst.extra, + }) + + +def create_instance_template(module, gce): + """Create an instance template + module : AnsibleModule object + gce: authenticated GCE libcloud driver + Returns: + instance template information + """ + # get info from module + name = module.params.get('name') + size = module.params.get('size') + source = module.params.get('source') + image = module.params.get('image') + image_family = module.params.get('image_family') + disk_type = module.params.get('disk_type') + disk_auto_delete = module.params.get('disk_auto_delete') + network = module.params.get('network') + subnetwork = module.params.get('subnetwork') + can_ip_forward = module.params.get('can_ip_forward') + external_ip = module.params.get('external_ip') + service_account_email = module.params.get('service_account_email') + service_account_permissions = module.params.get( + 'service_account_permissions') + on_host_maintenance = module.params.get('on_host_maintenance') + automatic_restart = module.params.get('automatic_restart') + preemptible = module.params.get('preemptible') + tags = module.params.get('tags') + metadata = module.params.get('metadata') + description = module.params.get('description') + disks = module.params.get('disks') + + changed = False + + # args of ex_create_instancetemplate + gce_args = dict( + name="instance", + size="f1-micro", + source=None, + image=None, + disk_type='pd-standard', + disk_auto_delete=True, + network='default', + subnetwork=None, + can_ip_forward=None, + external_ip='ephemeral', + service_accounts=None, + on_host_maintenance=None, + automatic_restart=None, + preemptible=None, + tags=None, + metadata=None, + description=None, + disks_gce_struct=None, + nic_gce_struct=None + ) + + gce_args['name'] = name + gce_args['size'] = size + + if source is not None: + gce_args['source'] = source + + if image: + gce_args['image'] = image + else: + if image_family: + image = gce.ex_get_image_from_family(image_family) + gce_args['image'] = image + else: + gce_args['image'] = "debian-8" + + gce_args['disk_type'] = disk_type + gce_args['disk_auto_delete'] = disk_auto_delete + + gce_network = gce.ex_get_network(network) + gce_args['network'] = gce_network + + if subnetwork is not None: + gce_args['subnetwork'] = subnetwork + + if can_ip_forward is not None: + gce_args['can_ip_forward'] = can_ip_forward + + if external_ip == "ephemeral": + instance_external_ip = external_ip + elif external_ip == "none": + instance_external_ip = None + else: + try: + instance_external_ip = gce.ex_get_address(external_ip) + except GoogleBaseError as err: + # external_ip is name ? + instance_external_ip = external_ip + gce_args['external_ip'] = instance_external_ip + + ex_sa_perms = [] + bad_perms = [] + if service_account_permissions: + for perm in service_account_permissions: + if perm not in gce.SA_SCOPES_MAP: + bad_perms.append(perm) + if len(bad_perms) > 0: + module.fail_json(msg='bad permissions: %s' % str(bad_perms)) + ex_sa_perms.append({'email': "default"}) + ex_sa_perms[0]['scopes'] = service_account_permissions + gce_args['service_accounts'] = ex_sa_perms + + if on_host_maintenance is not None: + gce_args['on_host_maintenance'] = on_host_maintenance + + if automatic_restart is not None: + gce_args['automatic_restart'] = automatic_restart + + if preemptible is not None: + gce_args['preemptible'] = preemptible + + if tags is not None: + gce_args['tags'] = tags + + # 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: + if isinstance(metadata, dict): + md = metadata + else: + try: + md = literal_eval(str(metadata)) + if not isinstance(md, dict): + raise ValueError('metadata must be a dict') + except ValueError as e: + module.fail_json(msg='bad metadata: %s' % str(e)) + except SyntaxError as e: + module.fail_json(msg='bad metadata syntax') + + if hasattr(libcloud, '__version__') and libcloud.__version__ < '0.15': + items = [] + for k, v in md.items(): + items.append({"key": k, "value": v}) + metadata = {'items': items} + else: + metadata = md + gce_args['metadata'] = metadata + + if description is not None: + gce_args['description'] = description + + instance = None + try: + instance = gce.ex_get_instancetemplate(name) + except ResourceNotFoundError: + try: + instance = gce.ex_create_instancetemplate(**gce_args) + changed = True + except GoogleBaseError as err: + module.fail_json( + msg='Unexpected error attempting to create instance {}, error: {}' + .format( + instance, + err.value + ) + ) + + if instance: + json_data = get_info(instance) + else: + module.fail_json(msg="no instance template!") + + return (changed, json_data, name) + + +def delete_instance_template(module, gce): + """ Delete instance template. + module : AnsibleModule object + gce: authenticated GCE libcloud driver + Returns: + instance template information + """ + name = module.params.get('name') + current_state = "absent" + changed = False + + # get instance template + instance = None + try: + instance = gce.ex_get_instancetemplate(name) + current_state = "present" + except GoogleBaseError as err: + json_data = dict(msg='instance template not exists') + + if current_state == "present": + rc = instance.destroy() + if rc: + changed = True + else: + module.fail_json( + msg='instance template destroy failed' + ) + + json_data = {} + return (changed, json_data, name) + + +def module_controller(module, gce): + ''' Control module state parameter. + module : AnsibleModule object + gce: authenticated GCE libcloud driver + Returns: + nothing + Exit: + AnsibleModule object exit with json data. + ''' + json_output = dict() + state = module.params.get("state") + if state == "present": + (changed, output, name) = create_instance_template(module, gce) + json_output['changed'] = changed + json_output['msg'] = output + elif state == "absent": + (changed, output, name) = delete_instance_template(module, gce) + json_output['changed'] = changed + json_output['msg'] = output + + module.exit_json(**json_output) + + +def check_if_system_state_would_be_changed(module, gce): + ''' check_if_system_state_would_be_changed ! + module : AnsibleModule object + gce: authenticated GCE libcloud driver + Returns: + system_state changed + ''' + changed = False + current_state = "absent" + + state = module.params.get("state") + name = module.params.get("name") + + instance = None + try: + instance = gce.ex_get_instancetemplate(name) + current_state = "present" + except GoogleBaseError as err: + module.fail_json(msg='GCE get instancetemplate problem') + + if current_state != state: + changed = True + + if current_state == "absent": + if changed: + output = 'instance template {} will be created'.format(name) + else: + output = 'nothing to do for instance template {} '.format(name) + if current_state == "present": + if changed: + output = 'instance template {} will be detroyed'.format(name) + else: + output = 'nothing to do for instance template {} '.format(name) + + return (changed, output) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + state=dict(choices=['present', 'absent'], default='present'), + name=dict(require=True, aliases=['base_name']), + size=dict(default='f1-micro'), + source=dict(), + image=dict(), + image_family=dict(default='debian-8'), + disk_type=dict(choices=['pd-standard', 'pd-ssd'], default='pd-standard', type='str'), + disk_auto_delete=dict(type='bool', default=True), + network=dict(default='default'), + subnetwork=dict(), + can_ip_forward=dict(type='bool', default=False), + external_ip=dict(default='ephemeral'), + service_account_email=dict(), + service_account_permissions=dict(type='list'), + automatic_restart=dict(type='bool', default=None), + preemptible=dict(type='bool', default=None), + tags=dict(type='list'), + metadata=dict(), + description=dict(), + disks=dict(type='list'), + nic_gce_struct=dict(type='list'), + project_id=dict(), + pem_file=dict(type='path'), + credentials_file=dict(type='path'), + ), + mutually_exclusive=[['source', 'image']], + required_one_of=[['image', 'image_family']], + supports_check_mode=True + ) + + if not HAS_PYTHON26: + module.fail_json( + msg="GCE module requires python's 'ast' module, python v2.6+") + if not HAS_LIBCLOUD: + module.fail_json( + msg='libcloud with GCE support (0.17.0+) required for this module') + + try: + gce = gce_connect(module) + except GoogleBaseError as err: + module.fail_json(msg='GCE Connexion failed') + + if module.check_mode: + (changed, output) = check_if_system_state_would_be_changed(module, gce) + module.exit_json( + changed=changed, + msg=output + ) + else: + module_controller(module, gce) + + +if __name__ == '__main__': + main()