mirror of https://github.com/ansible/ansible.git
[google] adding a GCE labels module
parent
35ba6d469e
commit
09e9203844
@ -0,0 +1,333 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# Copyright 2017 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/>.
|
||||||
|
|
||||||
|
ANSIBLE_METADATA = {'metadata_version': '1.0',
|
||||||
|
'status': ['preview'],
|
||||||
|
'supported_by': 'community'}
|
||||||
|
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: gce_labels
|
||||||
|
version_added: '2.4'
|
||||||
|
short_description: Create, Update or Destory GCE Labels.
|
||||||
|
description:
|
||||||
|
- Create, Update or Destory GCE Labels on instances, disks, snapshots, etc.
|
||||||
|
When specifying the GCE resource, users may specifiy the full URL for
|
||||||
|
the resource (its 'self_link'), or the individual parameters of the
|
||||||
|
resource (type, location, name). Examples for the two options can be
|
||||||
|
seen in the documentaion.
|
||||||
|
See U(https://cloud.google.com/compute/docs/label-or-tag-resources) for
|
||||||
|
more information about GCE Labels. Labels are gradually being added to
|
||||||
|
more GCE resources, so this module will need to be updated as new
|
||||||
|
resources are added to the GCE (v1) API.
|
||||||
|
requirements:
|
||||||
|
- 'python >= 2.6'
|
||||||
|
- 'google-api-python-client >= 1.6.2'
|
||||||
|
- 'google-auth >= 1.0.0'
|
||||||
|
- 'google-auth-httplib2 >= 0.0.2'
|
||||||
|
notes:
|
||||||
|
- Labels support resources such as instances, disks, images, etc. See
|
||||||
|
U(https://cloud.google.com/compute/docs/labeling-resources) for the list
|
||||||
|
of resources available in the GCE v1 API (not alpha or beta).
|
||||||
|
author:
|
||||||
|
- 'Eric Johnson (@erjohnso) <erjohnso@google.com>'
|
||||||
|
options:
|
||||||
|
labels:
|
||||||
|
description:
|
||||||
|
- A list of labels (key/value pairs) to add or remove for the resource.
|
||||||
|
required: false
|
||||||
|
resource_url:
|
||||||
|
description:
|
||||||
|
- The 'self_link' for the resource (instance, disk, snapshot, etc)
|
||||||
|
required: false
|
||||||
|
resource_type:
|
||||||
|
description:
|
||||||
|
- The type of resource (instances, disks, snapshots, images)
|
||||||
|
required: false
|
||||||
|
resource_location:
|
||||||
|
description:
|
||||||
|
- The location of resource (global, us-central1-f, etc.)
|
||||||
|
required: false
|
||||||
|
resource_name:
|
||||||
|
description:
|
||||||
|
- The name of resource.
|
||||||
|
required: false
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
- name: Add labels on an existing instance (using resource_url)
|
||||||
|
gce_labels:
|
||||||
|
service_account_email: "{{ service_account_email }}"
|
||||||
|
credentials_file: "{{ credentials_file }}"
|
||||||
|
project_id: "{{ project_id }}"
|
||||||
|
labels:
|
||||||
|
webserver-frontend: homepage
|
||||||
|
environment: test
|
||||||
|
experiment-name: kennedy
|
||||||
|
resource_url: https://www.googleapis.com/compute/beta/projects/myproject/zones/us-central1-f/instances/example-instance
|
||||||
|
state: present
|
||||||
|
- name: Add labels on an image (using resource params)
|
||||||
|
gce_labels:
|
||||||
|
service_account_email: "{{ service_account_email }}"
|
||||||
|
credentials_file: "{{ credentials_file }}"
|
||||||
|
project_id: "{{ project_id }}"
|
||||||
|
labels:
|
||||||
|
webserver-frontend: homepage
|
||||||
|
environment: test
|
||||||
|
experiment-name: kennedy
|
||||||
|
resource_type: images
|
||||||
|
resource_location: global
|
||||||
|
resource_name: my-custom-image
|
||||||
|
state: present
|
||||||
|
- name: Remove specified labels from the GCE instance
|
||||||
|
gce_labels:
|
||||||
|
service_account_email: "{{ service_account_email }}"
|
||||||
|
credentials_file: "{{ credentials_file }}"
|
||||||
|
project_id: "{{ project_id }}"
|
||||||
|
labels:
|
||||||
|
environment: prod
|
||||||
|
experiment-name: kennedy
|
||||||
|
resource_url: https://www.googleapis.com/compute/beta/projects/myproject/zones/us-central1-f/instances/example-instance
|
||||||
|
state: absent
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
labels:
|
||||||
|
description: List of labels that exist on the resource.
|
||||||
|
returned: Always.
|
||||||
|
type: dict
|
||||||
|
sample: [ { 'webserver-frontend': 'homepage', 'environment': 'test', 'environment-name': 'kennedy' } ]
|
||||||
|
resource_url:
|
||||||
|
description: The 'self_link' of the GCE resource.
|
||||||
|
returned: Always.
|
||||||
|
type: str
|
||||||
|
sample: 'https://www.googleapis.com/compute/beta/projects/myproject/zones/us-central1-f/instances/example-instance'
|
||||||
|
resource_type:
|
||||||
|
description: The type of the GCE resource.
|
||||||
|
returned: Always.
|
||||||
|
type: str
|
||||||
|
sample: instances
|
||||||
|
resource_location:
|
||||||
|
description: The location of the GCE resource.
|
||||||
|
returned: Always.
|
||||||
|
type: str
|
||||||
|
sample: us-central1-f
|
||||||
|
resource_name:
|
||||||
|
description: The name of the GCE resource.
|
||||||
|
returned: Always.
|
||||||
|
type: str
|
||||||
|
sample: my-happy-little-instance
|
||||||
|
state:
|
||||||
|
description: state of the labels
|
||||||
|
returned: Always.
|
||||||
|
type: str
|
||||||
|
sample: present
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ast import literal_eval
|
||||||
|
HAS_PYTHON26 = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_PYTHON26 = False
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
from ansible.module_utils.gcp import check_params, get_google_api_client, GCPUtils
|
||||||
|
|
||||||
|
UA_PRODUCT = 'ansible-gce_labels'
|
||||||
|
UA_VERSION = '0.0.1'
|
||||||
|
GCE_API_VERSION = 'v1'
|
||||||
|
|
||||||
|
# TODO(all): As Labels are added to more GCE resources, this list will need to
|
||||||
|
# be updated (along with some code changes below). The list can *only* include
|
||||||
|
# resources from the 'v1' GCE API and will *not* work with 'beta' or 'alpha'.
|
||||||
|
KNOWN_RESOURCES = ['instances', 'disks', 'snapshots', 'images']
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_resource(client, module):
|
||||||
|
params = module.params
|
||||||
|
if params['resource_url']:
|
||||||
|
if not params['resource_url'].startswith('https://www.googleapis.com/compute'):
|
||||||
|
module.fail_json(
|
||||||
|
msg='Invalid self_link url: %s' % params['resource_url'])
|
||||||
|
else:
|
||||||
|
parts = params['resource_url'].split('/')[8:]
|
||||||
|
if len(parts) == 2:
|
||||||
|
resource_type, resource_name = parts
|
||||||
|
resource_location = 'global'
|
||||||
|
else:
|
||||||
|
resource_location, resource_type, resource_name = parts
|
||||||
|
else:
|
||||||
|
if not params['resource_type'] or not params['resource_location'] \
|
||||||
|
or not params['resource_name']:
|
||||||
|
module.fail_json(msg='Missing required resource params.')
|
||||||
|
resource_type = params['resource_type'].lower()
|
||||||
|
resource_name = params['resource_name'].lower()
|
||||||
|
resource_location = params['resource_location'].lower()
|
||||||
|
|
||||||
|
if resource_type not in KNOWN_RESOURCES:
|
||||||
|
module.fail_json(msg='Unsupported resource_type: %s' % resource_type)
|
||||||
|
|
||||||
|
# TODO(all): See the comment above for KNOWN_RESOURCES. As labels are
|
||||||
|
# added to the v1 GCE API for more resources, some minor code work will
|
||||||
|
# need to be added here.
|
||||||
|
if resource_type == 'instances':
|
||||||
|
resource = client.instances().get(project=params['project_id'],
|
||||||
|
zone=resource_location,
|
||||||
|
instance=resource_name).execute()
|
||||||
|
elif resource_type == 'disks':
|
||||||
|
resource = client.disks().get(project=params['project_id'],
|
||||||
|
zone=resource_location,
|
||||||
|
disk=resource_name).execute()
|
||||||
|
elif resource_type == 'snapshots':
|
||||||
|
resource = client.snapshots().get(project=params['project_id'],
|
||||||
|
snapshot=resource_name).execute()
|
||||||
|
elif resource_type == 'images':
|
||||||
|
resource = client.images().get(project=params['project_id'],
|
||||||
|
image=resource_name).execute()
|
||||||
|
else:
|
||||||
|
module.fail_json(msg='Unsupported resource type: %s' % resource_type)
|
||||||
|
|
||||||
|
return resource.get('labelFingerprint', ''), {
|
||||||
|
'resource_name': resource.get('name'),
|
||||||
|
'resource_url': resource.get('selfLink'),
|
||||||
|
'resource_type': resource_type,
|
||||||
|
'resource_location': resource_location,
|
||||||
|
'labels': resource.get('labels', {})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _set_labels(client, new_labels, module, ri, fingerprint):
|
||||||
|
params = module.params
|
||||||
|
result = err = None
|
||||||
|
labels = {
|
||||||
|
'labels': new_labels,
|
||||||
|
'labelFingerprint': fingerprint
|
||||||
|
}
|
||||||
|
|
||||||
|
# TODO(all): See the comment above for KNOWN_RESOURCES. As labels are
|
||||||
|
# added to the v1 GCE API for more resources, some minor code work will
|
||||||
|
# need to be added here.
|
||||||
|
if ri['resource_type'] == 'instances':
|
||||||
|
req = client.instances().setLabels(project=params['project_id'],
|
||||||
|
instance=ri['resource_name'],
|
||||||
|
zone=ri['resource_location'],
|
||||||
|
body=labels)
|
||||||
|
elif ri['resource_type'] == 'disks':
|
||||||
|
req = client.disks().setLabels(project=params['project_id'],
|
||||||
|
zone=ri['resource_location'],
|
||||||
|
resource=ri['resource_name'],
|
||||||
|
body=labels)
|
||||||
|
elif ri['resource_type'] == 'snapshots':
|
||||||
|
req = client.snapshots().setLabels(project=params['project_id'],
|
||||||
|
resource=ri['resource_name'],
|
||||||
|
body=labels)
|
||||||
|
elif ri['resource_type'] == 'images':
|
||||||
|
req = client.images().setLabels(project=params['project_id'],
|
||||||
|
resource=ri['resource_name'],
|
||||||
|
body=labels)
|
||||||
|
else:
|
||||||
|
module.fail_json(msg='Unsupported resource type: %s' % ri['resource_type'])
|
||||||
|
|
||||||
|
# TODO(erjohnso): Once Labels goes GA, we'll be able to use the GCPUtils
|
||||||
|
# method to poll for the async request/operation to complete before
|
||||||
|
# returning. However, during 'beta', we are in an odd state where
|
||||||
|
# API requests must be sent to the 'compute/beta' API, but the python
|
||||||
|
# client library only allows for *Operations.get() requests to be
|
||||||
|
# sent to 'compute/v1' API. The response operation is in the 'beta'
|
||||||
|
# API-scope, but the client library cannot find the operation (404).
|
||||||
|
# result = GCPUtils.execute_api_client_req(req, client=client, raw=False)
|
||||||
|
# return result, err
|
||||||
|
result = req.execute()
|
||||||
|
return True, err
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
state=dict(choices=['absent', 'present'], default='present'),
|
||||||
|
service_account_email=dict(),
|
||||||
|
service_account_permissions=dict(type='list'),
|
||||||
|
pem_file=dict(),
|
||||||
|
credentials_file=dict(),
|
||||||
|
labels=dict(required=False, type='dict', default={}),
|
||||||
|
resource_url=dict(required=False, type='str'),
|
||||||
|
resource_name=dict(required=False, type='str'),
|
||||||
|
resource_location=dict(required=False, type='str'),
|
||||||
|
resource_type=dict(required=False, type='str'),
|
||||||
|
project_id=dict()
|
||||||
|
),
|
||||||
|
required_together=[
|
||||||
|
['resource_name', 'resource_location', 'resource_type']
|
||||||
|
],
|
||||||
|
mutually_exclusive=[
|
||||||
|
['resource_url', 'resource_name'],
|
||||||
|
['resource_url', 'resource_location'],
|
||||||
|
['resource_url', 'resource_type']
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if not HAS_PYTHON26:
|
||||||
|
module.fail_json(
|
||||||
|
msg="GCE module requires python's 'ast' module, python v2.6+")
|
||||||
|
|
||||||
|
client, cparams = get_google_api_client(module, 'compute',
|
||||||
|
user_agent_product=UA_PRODUCT,
|
||||||
|
user_agent_version=UA_VERSION,
|
||||||
|
api_version=GCE_API_VERSION)
|
||||||
|
|
||||||
|
# Get current resource info including labelFingerprint
|
||||||
|
fingerprint, resource_info = _fetch_resource(client, module)
|
||||||
|
new_labels = resource_info['labels'].copy()
|
||||||
|
|
||||||
|
update_needed = False
|
||||||
|
if module.params['state'] == 'absent':
|
||||||
|
for k, v in module.params['labels'].items():
|
||||||
|
if k in new_labels:
|
||||||
|
if new_labels[k] == v:
|
||||||
|
update_needed = True
|
||||||
|
new_labels.pop(k, None)
|
||||||
|
else:
|
||||||
|
module.fail_json(msg="Could not remove unmatched label pair '%s':'%s'" % (k, v))
|
||||||
|
else:
|
||||||
|
for k, v in module.params['labels'].items():
|
||||||
|
if k not in new_labels:
|
||||||
|
update_needed = True
|
||||||
|
new_labels[k] = v
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
json_output = {'state': module.params['state']}
|
||||||
|
if update_needed:
|
||||||
|
changed, err = _set_labels(client, new_labels, module, resource_info,
|
||||||
|
fingerprint)
|
||||||
|
json_output['changed'] = changed
|
||||||
|
|
||||||
|
# TODO(erjohnso): probably want to re-fetch the resource to return the
|
||||||
|
# new labelFingerprint, check that desired labels match updated labels.
|
||||||
|
# BUT! Will need to wait for setLabels() to hit v1 API so we can use the
|
||||||
|
# GCPUtils feature to poll for the operation to be complete. For now,
|
||||||
|
# we'll just update the output with what we have from the original
|
||||||
|
# state of the resource.
|
||||||
|
json_output.update(resource_info)
|
||||||
|
json_output.update(module.params)
|
||||||
|
|
||||||
|
module.exit_json(**json_output)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
# defaults file for test_gce_labels
|
||||||
|
instance_name: "{{ resource_prefix|lower }}"
|
||||||
|
service_account_email: "{{ gce_service_account_email }}"
|
||||||
|
pem_file: "{{ gce_pem_file }}"
|
||||||
|
project_id: "{{ gce_project_id }}"
|
||||||
|
zone: "us-central1-f"
|
||||||
|
machine_type: f1-micro
|
||||||
|
image: debian-8
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
# test role for gce_labels
|
||||||
|
- include: setup.yml
|
||||||
|
- include: test.yml
|
||||||
|
- include: teardown.yml
|
@ -0,0 +1,20 @@
|
|||||||
|
# GCE Labels Setup.
|
||||||
|
# ============================================================
|
||||||
|
- name: "Create instance for executing gce_labels tests"
|
||||||
|
gce:
|
||||||
|
instance_names: "{{ instance_name }}"
|
||||||
|
machine_type: "{{ machine_type }}"
|
||||||
|
image: "{{ image }}"
|
||||||
|
zone: "{{ zone }}"
|
||||||
|
project_id: "{{ project_id }}"
|
||||||
|
pem_file: "{{ pem_file }}"
|
||||||
|
service_account_email: "{{ service_account_email }}"
|
||||||
|
state: present
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert VM created
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- 'result.changed'
|
||||||
|
- 'result.instance_names[0] == "{{ instance_name }}"'
|
||||||
|
- 'result.state == "present"'
|
@ -0,0 +1,18 @@
|
|||||||
|
# GCE Labels Teardown.
|
||||||
|
# ============================================================
|
||||||
|
- name: "Teardown instance used in gce_labels test"
|
||||||
|
gce:
|
||||||
|
instance_names: "{{ instance_name }}"
|
||||||
|
zone: "{{ zone }}"
|
||||||
|
project_id: "{{ project_id }}"
|
||||||
|
pem_file: "{{ pem_file }}"
|
||||||
|
service_account_email: "{{ service_account_email }}"
|
||||||
|
state: absent
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert VM removed
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- 'result.changed'
|
||||||
|
- 'result.instance_names[0] == "{{ instance_name }}"'
|
||||||
|
- 'result.state == "absent"'
|
@ -0,0 +1,28 @@
|
|||||||
|
# GCE Labels Integration Tests.
|
||||||
|
|
||||||
|
## Parameter checking tests ##
|
||||||
|
# ============================================================
|
||||||
|
- name: "test unknown resource_type"
|
||||||
|
gce_labels:
|
||||||
|
service_account_email: "{{ service_account_email }}"
|
||||||
|
pem_file: "{{ pem_file }}"
|
||||||
|
project_id: "{{ project_id }}"
|
||||||
|
resource_type: doggie
|
||||||
|
resource_location: Kansas
|
||||||
|
resource_name: Toto
|
||||||
|
labels:
|
||||||
|
environment: dev
|
||||||
|
experiment: kennedy
|
||||||
|
register: result
|
||||||
|
ignore_errors: true
|
||||||
|
labels:
|
||||||
|
- param-check
|
||||||
|
|
||||||
|
- name: "assert failure when param: unknown resource_type"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- 'result.failed'
|
||||||
|
- 'result.msg == "Unsupported resource_type: doggie"'
|
||||||
|
|
||||||
|
|
||||||
|
# TODO(erjohnso): write more tests
|
Loading…
Reference in New Issue