From dec8ecda5a1661b492d24de8009638b1e0e87a6a Mon Sep 17 00:00:00 2001 From: Jakob Meng Date: Wed, 30 Nov 2022 10:17:08 +0100 Subject: [PATCH] Refactored coe_cluster{,_template} modules Change-Id: I209b242b43d8b79740752cd2c405705d247326c4 (cherry picked from commit 647ffef375da02a4b2d562d1f392dd25d8c86b89) --- .zuul.yaml | 47 + ci/roles/coe_cluster/defaults/main.yml | 18 + ci/roles/coe_cluster/tasks/main.yml | 181 ++++ .../coe_cluster_template/defaults/main.yml | 40 + ci/roles/coe_cluster_template/tasks/main.yml | 81 ++ ci/run-ansible-tests-collection.sh | 8 +- ci/run-collection.yml | 2 + plugins/modules/coe_cluster.py | 574 +++++++----- plugins/modules/coe_cluster_template.py | 853 +++++++++++------- 9 files changed, 1213 insertions(+), 591 deletions(-) create mode 100644 ci/roles/coe_cluster/defaults/main.yml create mode 100644 ci/roles/coe_cluster/tasks/main.yml create mode 100644 ci/roles/coe_cluster_template/defaults/main.yml create mode 100644 ci/roles/coe_cluster_template/tasks/main.yml diff --git a/.zuul.yaml b/.zuul.yaml index 4cb58167..246f9f9d 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -52,6 +52,49 @@ extensions_to_txt: log: true +- job: + name: ansible-collections-openstack-functional-devstack-magnum-base + parent: ansible-collections-openstack-functional-devstack-base + # Do not restrict branches in base jobs because else Zuul would not find a matching + # parent job variant during job freeze when child jobs are on other branches. + description: | + Run openstack collections functional tests against a devstack with Magnum plugin enabled + # Do not set job.override-checkout or job.required-projects.override-checkout in base job because + # else Zuul will use this branch when matching variants for parent jobs during job freeze + required-projects: + - openstack/magnum + - openstack/python-magnumclient + files: + - ^ci/roles/coe_cluster/.*$ + - ^plugins/modules/coe_cluster.py + - ^plugins/modules/coe_cluster_template.py + timeout: 10800 + vars: + devstack_localrc: + # NOTE: extend default glance limit from 1GB + GLANCE_LIMIT_IMAGE_SIZE_TOTAL: 5000 + devstack_plugins: + magnum: https://opendev.org/openstack/magnum + devstack_services: + magnum-api: true + magnum-cond: true + # Disable swift and dependent c-bak service to support upload of .qcow2.xz image in the gate + s-account: false + s-container: false + s-object: false + s-proxy: false + c-bak: false + tox_extra_args: -vv --skip-missing-interpreters=false -- coe_cluster coe_cluster_template + +- job: + name: ansible-collections-openstack-functional-devstack-magnum + parent: ansible-collections-openstack-functional-devstack-magnum-base + branches: master + description: | + Run openstack collections functional tests against a master devstack + with Magnum plugin enabled, using master of openstacksdk and latest + ansible release. Run it only on coe_cluster{,_template} changes. + - job: name: ansible-collections-openstack-functional-devstack-octavia-base parent: ansible-collections-openstack-functional-devstack-base @@ -537,6 +580,8 @@ dependencies: *deps_unit_lint - ansible-collections-openstack-functional-devstack-train-ansible-2.11: dependencies: *deps_unit_lint + - ansible-collections-openstack-functional-devstack-magnum: + dependencies: *deps_unit_lint - ansible-collections-openstack-functional-devstack-octavia: dependencies: *deps_unit_lint @@ -565,6 +610,7 @@ - ansible-collections-openstack-functional-devstack-wallaby-ansible-2.12 - ansible-collections-openstack-functional-devstack-xena-ansible-2.12 - ansible-collections-openstack-functional-devstack-train-ansible-2.11 + - ansible-collections-openstack-functional-devstack-magnum - ansible-collections-openstack-functional-devstack-octavia - tripleo-ci-centos-8-standalone-osa-wallaby @@ -582,6 +628,7 @@ - ansible-collections-openstack-functional-devstack-train-ansible-2.11 - bifrost-collections-src - bifrost-keystone-collections-src + - ansible-collections-openstack-functional-devstack-magnum - ansible-collections-openstack-functional-devstack-octavia - tripleo-ci-centos-9-standalone-osa-wallaby - tripleo-ci-centos-8-standalone-osa-wallaby diff --git a/ci/roles/coe_cluster/defaults/main.yml b/ci/roles/coe_cluster/defaults/main.yml new file mode 100644 index 00000000..fd61a2b2 --- /dev/null +++ b/ci/roles/coe_cluster/defaults/main.yml @@ -0,0 +1,18 @@ +expected_fields: +# Magnum might return more fields according to its documentation [0] but +# openstacksdk normalizes coe cluster resources, moving most fields from +# top level into a 'properties' field [1]. +# [0] https://docs.openstack.org/api-ref/container-infrastructure-management/#create-new-cluster +# [1] https://opendev.org/openstack/openstacksdk/src/commit/d57c1fcab3b6cbe806cbae735fefa4983b200ab2/openstack/cloud/_normalize.py#L484 + - cluster_template_id + - create_timeout + - id + - keypair + - location + - master_count + - name + - node_count + - properties + - stack_id + - status + - uuid diff --git a/ci/roles/coe_cluster/tasks/main.yml b/ci/roles/coe_cluster/tasks/main.yml new file mode 100644 index 00000000..ac2beb55 --- /dev/null +++ b/ci/roles/coe_cluster/tasks/main.yml @@ -0,0 +1,181 @@ +--- +- name: Create keypair + openstack.cloud.keypair: + cloud: "{{ cloud }}" + name: ansible_keypair + state: present + register: keypair + +- name: List all images + openstack.cloud.image_info: + cloud: "{{ cloud }}" + register: images + +- name: Identify Fedora CoreOS image id + set_fact: + image_id: "{{ images.images|community.general.json_query(query)|first }}" + vars: + query: "[?starts_with(name, 'fedora-coreos')].id" + +- name: Create external network + openstack.cloud.network: + cloud: "{{ cloud }}" + external: true + name: ansible_external_network + state: present + register: external_network + +- name: Create external subnet + openstack.cloud.subnet: + cidr: 10.6.6.0/24 + cloud: "{{ cloud }}" + name: ansible_external_subnet + network_name: ansible_external_network + state: present + +- name: Create internal network + openstack.cloud.network: + cloud: "{{ cloud }}" + state: present + name: ansible_internal_network + external: false + +- name: Create internal subnet + openstack.cloud.subnet: + cloud: "{{ cloud }}" + state: present + network_name: ansible_internal_network + name: ansible_internal_subnet + cidr: 10.7.7.0/24 + +- name: Create router + openstack.cloud.router: + cloud: "{{ cloud }}" + external_fixed_ips: + - subnet: ansible_external_subnet + ip: 10.6.6.10 + interfaces: + - net: ansible_internal_network + subnet: ansible_internal_subnet + portip: 10.7.7.1 + name: ansible_router + network: ansible_external_network + state: present + +- name: Create Kubernetes cluster template + openstack.cloud.coe_cluster_template: + cloud: "{{ cloud }}" + coe: kubernetes + external_network_id: '{{ external_network.network.id }}' + fixed_network: ansible_internal_network + fixed_subnet: ansible_internal_subnet + floating_ip_enabled: true + image_id: '{{ image_id }}' + keypair_id: '{{ keypair.keypair.id }}' + name: k8s + state: present + register: coe_cluster_template + +- name: Create Kubernetes cluster + openstack.cloud.coe_cluster: + cloud: "{{ cloud }}" + cluster_template_id: "{{ coe_cluster_template.cluster_template.uuid }}" + keypair: ansible_keypair + name: k8s + state: present + # cluster creation takes longer than max tenant timeout of 10800 + wait: false + register: coe_cluster + +- name: Assert return values of coe_cluster module + assert: + that: + # openstack.cloud.coe_cluster will only return 'id' on cluster creation when wait is false + - "['id']|difference(coe_cluster.cluster.keys())|length == 0" + +- name: Pause for 1 minutes to allow Magnum to create the Kubernetes cluster + ansible.builtin.pause: + minutes: 1 + +- name: Create Kubernetes cluster again + openstack.cloud.coe_cluster: + cloud: "{{ cloud }}" + cluster_template_id: "{{ coe_cluster_template.cluster_template.uuid }}" + keypair: ansible_keypair + name: k8s + state: present + # cluster creation takes longer than max tenant timeout of 10800 + wait: false + register: coe_cluster + +- name: Assert return values of coe_cluster module + assert: + that: + # allow new fields to be introduced but prevent fields from being removed + - expected_fields|difference(coe_cluster.cluster.keys())|length == 0 + +- name: Delete Kubernetes cluster + openstack.cloud.coe_cluster: + cloud: "{{ cloud }}" + name: k8s + state: absent + register: coe_cluster + +- name: Assert return values of coe_cluster module + assert: + that: + - coe_cluster is changed + +- name: Delete Kubernetes cluster again + openstack.cloud.coe_cluster: + cloud: "{{ cloud }}" + name: k8s + state: absent + register: coe_cluster + +- name: Assert return values of coe_cluster module + assert: + that: + - coe_cluster is not changed + +- name: Delete Kubernetes cluster template + openstack.cloud.coe_cluster_template: + cloud: "{{ cloud }}" + name: k8s + state: absent + +- name: Delete router + openstack.cloud.router: + cloud: "{{ cloud }}" + name: ansible_router + state: absent + +- name: Delete internal subnet + openstack.cloud.subnet: + cloud: "{{ cloud }}" + state: absent + name: ansible_internal_subnet + +- name: Delete internal network + openstack.cloud.network: + cloud: "{{ cloud }}" + state: absent + name: ansible_internal_network + +- name: Delete external subnet + openstack.cloud.subnet: + cloud: "{{ cloud }}" + name: ansible_external_subnet + state: absent + +- name: Delete external network + openstack.cloud.network: + cloud: "{{ cloud }}" + name: ansible_external_network + state: absent + +- name: Delete keypair + openstack.cloud.keypair: + cloud: "{{ cloud }}" + name: ansible_keypair + state: absent diff --git a/ci/roles/coe_cluster_template/defaults/main.yml b/ci/roles/coe_cluster_template/defaults/main.yml new file mode 100644 index 00000000..13ea5fb0 --- /dev/null +++ b/ci/roles/coe_cluster_template/defaults/main.yml @@ -0,0 +1,40 @@ +expected_fields: +# Magnum might return more fields according to its documentation [0] but +# openstacksdk normalizes coe cluster template resources, moving most +# fields from top level into a 'properties' field [1]. +# [0] https://docs.openstack.org/api-ref/container-infrastructure-management/#create-new-cluster +# [1] https://opendev.org/openstack/openstacksdk/src/commit/d57c1fcab3b6cbe806cbae735fefa4983b200ab2/openstack/cloud/_normalize.py#L522 + - apiserver_port + - cluster_distro + - coe + - created_at + - dns_nameserver + - docker_volume_size + - external_network_id + - fixed_network + - fixed_subnet + - flavor_id + - floating_ip_enabled + - http_proxy + - https_proxy + - id + - image_id + - insecure_registry + - is_public + - is_registry_enabled + - is_tls_disabled + - keypair_id + - labels + - location + - master_flavor_id + - name + - network_driver + - no_proxy + - properties + - public + - registry_enabled + - server_type + - tls_disabled + - updated_at + - uuid + - volume_driver diff --git a/ci/roles/coe_cluster_template/tasks/main.yml b/ci/roles/coe_cluster_template/tasks/main.yml new file mode 100644 index 00000000..8b61e156 --- /dev/null +++ b/ci/roles/coe_cluster_template/tasks/main.yml @@ -0,0 +1,81 @@ +--- +- name: Create keypair + openstack.cloud.keypair: + cloud: "{{ cloud }}" + name: ansible_keypair + state: present + register: keypair + +- name: List all images + openstack.cloud.image_info: + cloud: "{{ cloud }}" + register: images + +- name: Identify Fedora CoreOS image id + set_fact: + image_id: "{{ images.images|community.general.json_query(query)|first }}" + vars: + query: "[?starts_with(name, 'fedora-coreos')].id" + +- name: Create Kubernetes cluster template + openstack.cloud.coe_cluster_template: + cloud: "{{ cloud }}" + coe: kubernetes + floating_ip_enabled: false + image_id: '{{ image_id }}' + keypair_id: '{{ keypair.keypair.id }}' + name: k8s + state: present + register: coe_cluster_template + +- name: Assert return values of coe_cluster_template module + assert: + that: + # allow new fields to be introduced but prevent fields from being removed + - expected_fields|difference(coe_cluster_template.cluster_template.keys())|length == 0 + +- name: Create Kubernetes cluster template again + openstack.cloud.coe_cluster_template: + cloud: "{{ cloud }}" + coe: kubernetes + floating_ip_enabled: false + image_id: '{{ image_id }}' + keypair_id: '{{ keypair.keypair.id }}' + name: k8s + state: present + register: coe_cluster_template + +- name: Assert return values of coe_cluster_template module + assert: + that: + - coe_cluster_template is not changed + +- name: Delete Kubernetes cluster template + openstack.cloud.coe_cluster_template: + cloud: "{{ cloud }}" + name: k8s + state: absent + register: coe_cluster_template + +- name: Assert return values of coe_cluster_template module + assert: + that: + - coe_cluster_template is changed + +- name: Delete Kubernetes cluster template again + openstack.cloud.coe_cluster_template: + cloud: "{{ cloud }}" + name: k8s + state: absent + register: coe_cluster_template + +- name: Assert return values of coe_cluster_template module + assert: + that: + - coe_cluster_template is not changed + +- name: Delete keypair + openstack.cloud.keypair: + cloud: "{{ cloud }}" + name: ansible_keypair + state: absent diff --git a/ci/run-ansible-tests-collection.sh b/ci/run-ansible-tests-collection.sh index 9ee78097..af03dad4 100755 --- a/ci/run-ansible-tests-collection.sh +++ b/ci/run-ansible-tests-collection.sh @@ -114,10 +114,16 @@ if [ -n "$TAGS" ]; then fi if ! systemctl is-enabled devstack@o-api.service 2>&1; then - # Run all tasks except for loadbalancer if Octavia is not available + # Skip loadbalancer tasks if Octavia is not available tag_opt+=" --skip-tags loadbalancer" fi +# TODO: Replace with more robust test for Magnum availability +if [ ! -e /etc/magnum ]; then + # Skip coe tasks if Magnum is not available + tag_opt+=" --skip-tags coe_cluster,coe_cluster_template" +fi + cd ci/ # Run tests diff --git a/ci/run-collection.yml b/ci/run-collection.yml index b688b61f..0eb4e1b0 100644 --- a/ci/run-collection.yml +++ b/ci/run-collection.yml @@ -8,6 +8,8 @@ - { role: auth, tags: auth } - { role: catalog_service, tags: catalog_service } - { role: client_config, tags: client_config } + - { role: coe_cluster, tags: coe_cluster } + - { role: coe_cluster_template, tags: coe_cluster_template } - { role: dns_zone_info, tags: dns_zone_info } - role: object_container tags: object_container diff --git a/plugins/modules/coe_cluster.py b/plugins/modules/coe_cluster.py index feb202a3..b12e2ab5 100644 --- a/plugins/modules/coe_cluster.py +++ b/plugins/modules/coe_cluster.py @@ -3,288 +3,366 @@ # Copyright (c) 2018 Catalyst IT Ltd. # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -DOCUMENTATION = ''' +DOCUMENTATION = r''' --- module: coe_cluster -short_description: Add/Remove COE cluster from OpenStack Cloud +short_description: Manage COE cluster in OpenStack Cloud author: OpenStack Ansible SIG description: - - Add or Remove COE cluster from the OpenStack Container Infra service. + - Add or remove a COE (Container Orchestration Engine) cluster + via OpenStack's Magnum aka Container Infrastructure Management API. options: - cluster_template_id: - description: - - The template ID of cluster template. - required: true - type: str - discovery_url: - description: - - Url used for cluster node discovery - type: str - docker_volume_size: - description: - - The size in GB of the docker volume - type: int - flavor_id: - description: - - The flavor of the minion node for this ClusterTemplate - type: str - keypair: - description: - - Name of the keypair to use. - type: str - labels: - description: - - One or more key/value pairs - type: raw - master_flavor_id: - description: - - The flavor of the master node for this ClusterTemplate - type: str - master_count: - description: - - The number of master nodes for this cluster - default: 1 - type: int - name: - description: - - Name that has to be given to the cluster template - required: true - type: str - node_count: - description: - - The number of nodes for this cluster - default: 1 - type: int - state: - description: - - Indicate desired state of the resource. - choices: [present, absent] - default: present - type: str - timeout: - description: - - Timeout for creating the cluster in minutes. Default to 60 mins - if not set - default: 60 - type: int -requirements: - - "python >= 3.6" - - "openstacksdk" - -extends_documentation_fragment: -- openstack.cloud.openstack -''' - -RETURN = ''' -id: - description: The cluster UUID. - returned: On success when I(state) is 'present' + cluster_template_id: + description: + - The template ID of cluster template. + - Required if I(state) is C(present). type: str - sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" -cluster: - description: Dictionary describing the cluster. - returned: On success when I(state) is 'present' - type: complex - contains: - api_address: - description: - - Api address of cluster master node - type: str - sample: https://172.24.4.30:6443 - cluster_template_id: - description: The cluster_template UUID - type: str - sample: '7b1418c8-cea8-48fc-995d-52b66af9a9aa' - coe_version: - description: - - Version of the COE software currently running in this cluster - type: str - sample: v1.11.1 - container_version: - description: - - "Version of the container software. Example: docker version." - type: str - sample: 1.12.6 - created_at: - description: - - The time in UTC at which the cluster is created - type: str - sample: "2018-08-16T10:29:45+00:00" - create_timeout: - description: - - Timeout for creating the cluster in minutes. Default to 60 if - not set. - type: int - sample: 60 - discovery_url: - description: - - Url used for cluster node discovery - type: str - sample: https://discovery.etcd.io/a42ee38e7113f31f4d6324f24367aae5 - faults: - description: - - Fault info collected from the Heat resources of this cluster - type: dict - sample: {'0': 'ResourceInError: resources[0].resources...'} - flavor_id: - description: - - The flavor of the minion node for this cluster - type: str - sample: c1.c1r1 - keypair: - description: - - Name of the keypair to use. - type: str - sample: mykey - labels: - description: One or more key/value pairs - type: dict - sample: {'key1': 'value1', 'key2': 'value2'} - master_addresses: - description: - - IP addresses of cluster master nodes - type: list - sample: ['172.24.4.5'] - master_count: - description: - - The number of master nodes for this cluster. - type: int - sample: 1 - master_flavor_id: - description: - - The flavor of the master node for this cluster - type: str - sample: c1.c1r1 - name: - description: - - Name that has to be given to the cluster - type: str - sample: k8scluster - node_addresses: - description: - - IP addresses of cluster slave nodes - type: list - sample: ['172.24.4.8'] - node_count: - description: - - The number of master nodes for this cluster. - type: int - sample: 1 - stack_id: - description: - - Stack id of the Heat stack - type: str - sample: '07767ec6-85f5-44cb-bd63-242a8e7f0d9d' - status: - description: Status of the cluster from the heat stack - type: str - sample: 'CREATE_COMLETE' - status_reason: - description: - - Status reason of the cluster from the heat stack - type: str - sample: 'Stack CREATE completed successfully' - updated_at: - description: - - The time in UTC at which the cluster is updated - type: str - sample: '2018-08-16T10:39:25+00:00' - id: - description: - - Unique UUID for this cluster - type: str - sample: '86246a4d-a16c-4a58-9e96ad7719fe0f9d' + discovery_url: + description: + - URL used for cluster node discovery. + type: str + docker_volume_size: + description: + - The size in GB of the docker volume. + type: int + flavor_id: + description: + - The flavor of the minion node for this cluster template. + type: str + floating_ip_enabled: + description: + - Indicates whether created cluster should have a floating ip. + - Whether enable or not using the floating IP of cloud provider. Some + cloud providers used floating IP, some used public IP, thus Magnum + provide this option for specifying the choice of using floating IP. + - If not set, the value of I(floating_ip_enabled) of the cluster template + specified with I(cluster_template_id) will be used. + - When I(floating_ip_enabled) is set to C(true), then + I(external_network_id) in cluster template must be defined. + type: bool + keypair: + description: + - Name of the keypair to use. + type: str + labels: + description: + - One or more key/value pairs. + type: raw + master_count: + description: + - The number of master nodes for this cluster. + - Magnum's default value for I(master_count) is 1. + type: int + master_flavor_id: + description: + - The flavor of the master node for this cluster template. + type: str + name: + description: + - Name that has to be given to the cluster template. + required: true + type: str + node_count: + description: + - The number of nodes for this cluster. + - Magnum's default value for I(node_count) is 1. + type: int + state: + description: + - Indicate desired state of the resource. + choices: [present, absent] + default: present + type: str +notes: + - Return values of this module are preliminary and will most likely change + when openstacksdk has finished its transition of cloud layer functions to + resource proxies. +requirements: + - "python >= 3.6" + - "openstacksdk" +extends_documentation_fragment: + - openstack.cloud.openstack ''' -EXAMPLES = ''' -# Create a new Kubernetes cluster -- openstack.cloud.coe_cluster: - name: k8s +RETURN = r''' +id: + description: The cluster UUID. + returned: On success when I(state) is C(present). + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" +cluster: + description: Dictionary describing the cluster. + returned: On success when I(state) is C(present). + type: complex + contains: + cluster_template_id: + description: The cluster_template UUID + type: str + sample: '7b1418c8-cea8-48fc-995d-52b66af9a9aa' + create_timeout: + description: Timeout for creating the cluster in minutes. + Default to 60 if not set. + type: int + sample: 60 + id: + description: Unique UUID for this cluster. + type: str + sample: '86246a4d-a16c-4a58-9e96ad7719fe0f9d' + keypair: + description: Name of the keypair to use. + type: str + sample: mykey + location: + description: The OpenStack location of this resource. + type: str + master_count: + description: The number of master nodes for this cluster. + type: int + sample: 1 + name: + description: Name that has to be given to the cluster. + type: str + sample: k8scluster + node_count: + description: The number of master nodes for this cluster. + type: int + sample: 1 + properties: + description: Additional properties of the cluster template. + type: dict + sample: | + { + 'api_address': 'https://172.24.4.30:6443', + 'coe_version': 'v1.11.1', + 'container_version': '1.12.6', + 'created_at': '2018-08-16T10:29:45+00:00', + 'discovery_url': 'https://discovery.etcd.io/a42...aae5', + 'faults': {'0': 'ResourceInError: resources[0].resources...'}, + 'flavor_id': 'c1.c1r1', + 'floating_ip_enabled': true, + 'labels': {'key1': 'value1', 'key2': 'value2'}, + 'master_addresses': ['172.24.4.5'], + 'master_flavor_id': 'c1.c1r1', + 'node_addresses': ['172.24.4.8'], + 'status_reason': 'Stack CREATE completed successfully', + 'updated_at': '2018-08-16T10:39:25+00:00', + } + stack_id: + description: Stack id of the Heat stack. + type: str + sample: '07767ec6-85f5-44cb-bd63-242a8e7f0d9d' + status: + description: Status of the cluster from the heat stack. + type: str + sample: 'CREATE_COMLETE' + uuid: + description: Unique UUID for this cluster. + type: str + sample: '86246a4d-a16c-4a58-9e96ad7719fe0f9d' +''' + +EXAMPLES = r''' +- name: Create a new Kubernetes cluster + openstack.cloud.coe_cluster: + cloud: devstack cluster_template_id: k8s-ha keypair: mykey master_count: 3 + name: k8s node_count: 5 ''' from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule -class CoeClusterModule(OpenStackModule): +class COEClusterModule(OpenStackModule): argument_spec = dict( - cluster_template_id=dict(required=True), - discovery_url=dict(default=None), + cluster_template_id=dict(), + discovery_url=dict(), docker_volume_size=dict(type='int'), - flavor_id=dict(default=None), - keypair=dict(default=None, no_log=False), - labels=dict(default=None, type='raw'), - master_count=dict(type='int', default=1), - master_flavor_id=dict(default=None), + flavor_id=dict(), + floating_ip_enabled=dict(type='bool'), + keypair=dict(no_log=False), # := noqa no-log-needed + labels=dict(type='raw'), + master_count=dict(type='int'), + master_flavor_id=dict(), name=dict(required=True), - node_count=dict(type='int', default=1), + node_count=dict(type='int'), state=dict(default='present', choices=['absent', 'present']), - timeout=dict(type='int', default=60), ) - module_kwargs = dict() - - def _parse_labels(self, labels): - if isinstance(labels, str): - labels_dict = {} - for kv_str in labels.split(","): - k, v = kv_str.split("=") - labels_dict[k] = v - return labels_dict - if not labels: - return {} - return labels + module_kwargs = dict( + required_if=[ + ('state', 'present', ('cluster_template_id',)) + ], + supports_check_mode=True, + ) def run(self): - params = self.params.copy() - state = self.params['state'] + + cluster = self._find() + + if self.ansible.check_mode: + self.exit_json(changed=self._will_change(state, cluster)) + + if state == 'present' and not cluster: + # Create cluster + cluster = self._create() + self.exit_json(changed=True, + # for backward compatibility + id=cluster['id'], + cluster=cluster) + + elif state == 'present' and cluster: + # Update cluster + update = self._build_update(cluster) + if update: + cluster = self._update(cluster, update) + + self.exit_json(changed=bool(update), + # for backward compatibility + id=cluster['id'], + cluster=cluster) + + elif state == 'absent' and cluster: + # Delete cluster + self._delete(cluster) + self.exit_json(changed=True) + + elif state == 'absent' and not cluster: + # Do nothing + self.exit_json(changed=False) + + def _build_update(self, cluster): + update = {} + + # TODO: Implement support for updates. + non_updateable_keys = [k for k in ['cluster_template_id', + 'discovery_url', + 'docker_volume_size', 'flavor_id', + 'floating_ip_enabled', 'keypair', + 'master_count', 'master_flavor_id', + 'name', 'node_count'] + if self.params[k] is not None + and self.params[k] != cluster[k]] + + labels = self.params['labels'] + if labels is not None: + if isinstance(labels, str): + labels = dict([tuple(kv.split(":")) + for kv in labels.split(",")]) + if labels != cluster['labels']: + non_updateable_keys.append('labels') + + if non_updateable_keys: + self.fail_json(msg='Cannot update parameters {0}' + .format(non_updateable_keys)) + + attributes = dict((k, self.params[k]) + for k in [] + if self.params[k] is not None + and self.params[k] != cluster[k]) + + if attributes: + update['attributes'] = attributes + + return update + + def _create(self): + # TODO: Complement *_id parameters with find_* functions to allow + # specifying names in addition to IDs. + kwargs = dict((k, self.params[k]) + for k in ['cluster_template_id', 'discovery_url', + 'docker_volume_size', 'flavor_id', + 'floating_ip_enabled', 'keypair', + 'master_count', 'master_flavor_id', + 'name', 'node_count'] + if self.params[k] is not None) + + labels = self.params['labels'] + if labels is not None: + if isinstance(labels, str): + labels = dict([tuple(kv.split(":")) + for kv in labels.split(",")]) + kwargs['labels'] = labels + + kwargs['create_timeout'] = self.params['timeout'] + + cluster = self.conn.create_coe_cluster(**kwargs) + + if not self.params['wait']: + # openstacksdk's create_coe_cluster() returns a cluster's uuid only + # but we cannot use self.conn.get_coe_cluster(cluster_id) because + # it might return None as long as the cluster is being set up. + return cluster + + cluster_id = cluster['id'] + + if self.params['wait']: + for count in self.sdk.utils.iterate_timeout( + timeout=self.params['timeout'], + message="Timeout waiting for cluster to be present" + ): + # Fetch cluster again + cluster = self.conn.get_coe_cluster(cluster_id) + + if cluster is None: + continue + elif cluster.status.lower() == 'active': + break + elif cluster.status.lower() in ['error']: + self.fail_json(msg="{0} transitioned to failure state {1}" + .format(cluster.name, 'error')) + + return cluster + + def _delete(self, cluster): + self.conn.delete_coe_cluster(cluster.name) + + if self.params['wait']: + for count in self.sdk.utils.iterate_timeout( + timeout=self.params['timeout'], + message="Timeout waiting for cluster to be absent" + ): + cluster = self.conn.get_coe_cluster(cluster.id) + if cluster is None: + break + elif cluster['status'].lower() == 'deleted': + break + + def _find(self): name = self.params['name'] + filters = {} + cluster_template_id = self.params['cluster_template_id'] + if cluster_template_id is not None: + filters['cluster_template_id'] = cluster_template_id - kwargs = dict( - discovery_url=self.params['discovery_url'], - docker_volume_size=self.params['docker_volume_size'], - flavor_id=self.params['flavor_id'], - keypair=self.params['keypair'], - labels=self._parse_labels(params['labels']), - master_count=self.params['master_count'], - master_flavor_id=self.params['master_flavor_id'], - node_count=self.params['node_count'], - create_timeout=self.params['timeout'], - ) + return self.conn.get_coe_cluster(name_or_id=name, filters=filters) - changed = False - cluster = self.conn.get_coe_cluster( - name_or_id=name, filters={'cluster_template_id': cluster_template_id}) + def _update(self, cluster, update): + attributes = update.get('attributes') + if attributes: + # TODO: Implement support for updates. + # cluster = self.conn.update_coe_cluster(...) + pass - if state == 'present': - if not cluster: - cluster = self.conn.create_coe_cluster( - name, cluster_template_id=cluster_template_id, **kwargs) - changed = True - else: - changed = False + return cluster - # NOTE (brtknr): At present, create_coe_cluster request returns - # cluster_id as `uuid` whereas get_coe_cluster request returns the - # same field as `id`. This behaviour may change in the future - # therefore try `id` first then `uuid`. - cluster_id = cluster.get('id', cluster.get('uuid')) - cluster['id'] = cluster['uuid'] = cluster_id - self.exit_json(changed=changed, cluster=cluster, id=cluster_id) - elif state == 'absent': - if not cluster: - self.exit_json(changed=False) - else: - self.conn.delete_coe_cluster(name) - self.exit_json(changed=True) + def _will_change(self, state, cluster): + if state == 'present' and not cluster: + return True + elif state == 'present' and cluster: + return bool(self._build_update(cluster)) + elif state == 'absent' and cluster: + return True + else: + # state == 'absent' and not cluster: + return False def main(): - module = CoeClusterModule() + module = COEClusterModule() module() diff --git a/plugins/modules/coe_cluster_template.py b/plugins/modules/coe_cluster_template.py index 0596f39b..fb14fee8 100644 --- a/plugins/modules/coe_cluster_template.py +++ b/plugins/modules/coe_cluster_template.py @@ -3,384 +3,553 @@ # Copyright (c) 2018 Catalyst IT Ltd. # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -DOCUMENTATION = ''' +DOCUMENTATION = r''' --- module: coe_cluster_template -short_description: Add/Remove COE cluster template from OpenStack Cloud +short_description: Manage COE cluster template in OpenStack Cloud author: OpenStack Ansible SIG description: - - Add or Remove COE cluster template from the OpenStack Container Infra - service. + - Add or remove a COE (Container Orchestration Engine) cluster template + via OpenStack's Magnum aka Container Infrastructure Management API. options: - coe: - description: - - The Container Orchestration Engine for this clustertemplate - choices: [kubernetes, swarm, mesos] - type: str - required: true - dns_nameserver: - description: - - The DNS nameserver address - default: '8.8.8.8' - type: str - docker_storage_driver: - description: - - Docker storage driver - choices: [devicemapper, overlay, overlay2] - type: str - docker_volume_size: - description: - - The size in GB of the docker volume - type: int - external_network_id: - description: - - The external network to attach to the Cluster - type: str - fixed_network: - description: - - The fixed network name to attach to the Cluster - type: str - fixed_subnet: - description: - - The fixed subnet name to attach to the Cluster - type: str - flavor_id: - description: - - The flavor of the minion node for this ClusterTemplate - type: str - floating_ip_enabled: - description: - - Indicates whether created clusters should have a floating ip or not - type: bool - default: true - keypair_id: - description: - - Name or ID of the keypair to use. - type: str - image_id: - description: - - Image id the cluster will be based on - type: str - required: true - labels: - description: - - One or more key/value pairs - type: raw - http_proxy: - description: - - Address of a proxy that will receive all HTTP requests and relay them - The format is a URL including a port number - type: str - https_proxy: - description: - - Address of a proxy that will receive all HTTPS requests and relay - them. The format is a URL including a port number - type: str - master_flavor_id: - description: - - The flavor of the master node for this ClusterTemplate - type: str - master_lb_enabled: - description: - - Indicates whether created clusters should have a load balancer - for master nodes or not - type: bool - default: 'no' - name: - description: - - Name that has to be given to the cluster template - required: true - type: str - network_driver: - description: - - The name of the driver used for instantiating container networks - choices: [flannel, calico, docker] - type: str - no_proxy: - description: - - A comma separated list of IPs for which proxies should not be - used in the cluster - type: str - public: - description: - - Indicates whether the ClusterTemplate is public or not - type: bool - default: 'no' - registry_enabled: - description: - - Indicates whether the docker registry is enabled - type: bool - default: 'no' - server_type: - description: - - Server type for this ClusterTemplate - choices: [vm, bm] - default: vm - type: str - state: - description: - - Indicate desired state of the resource. - choices: [present, absent] - default: present - type: str - tls_disabled: - description: - - Indicates whether the TLS should be disabled - type: bool - default: 'no' - volume_driver: - description: - - The name of the driver used for instantiating container volumes - choices: [cinder, rexray] - type: str -requirements: - - "python >= 3.6" - - "openstacksdk" - -extends_documentation_fragment: -- openstack.cloud.openstack -''' - -RETURN = ''' -id: - description: The cluster UUID. - returned: On success when I(state) is 'present' + coe: + description: + - The Container Orchestration Engine for this cluster template + - Required if I(state) is C(present). + choices: [kubernetes, swarm, mesos] type: str - sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" -cluster_template: - description: Dictionary describing the template. - returned: On success when I(state) is 'present' - type: complex - contains: - coe: - description: The Container Orchestration Engine for this clustertemplate - type: str - sample: kubernetes - dns_nameserver: - description: The DNS nameserver address - type: str - sample: '8.8.8.8' - docker_storage_driver: - description: Docker storage driver - type: str - sample: devicemapper - docker_volume_size: - description: The size in GB of the docker volume - type: int - sample: 5 - external_network_id: - description: The external network to attach to the Cluster - type: str - sample: public - fixed_network: - description: The fixed network name to attach to the Cluster - type: str - sample: 07767ec6-85f5-44cb-bd63-242a8e7f0d9d - fixed_subnet: - description: - - The fixed subnet name to attach to the Cluster - type: str - sample: 05567ec6-85f5-44cb-bd63-242a8e7f0d9d - flavor_id: - description: - - The flavor of the minion node for this ClusterTemplate - type: str - sample: c1.c1r1 - floating_ip_enabled: - description: - - Indicates whether created clusters should have a floating ip or not - type: bool - sample: true - keypair_id: - description: - - Name or ID of the keypair to use. - type: str - sample: mykey - image_id: - description: - - Image id the cluster will be based on - type: str - sample: 05567ec6-85f5-44cb-bd63-242a8e7f0e9d - labels: - description: One or more key/value pairs - type: dict - sample: {'key1': 'value1', 'key2': 'value2'} - http_proxy: - description: - - Address of a proxy that will receive all HTTP requests and relay them - The format is a URL including a port number - type: str - sample: http://10.0.0.11:9090 - https_proxy: - description: - - Address of a proxy that will receive all HTTPS requests and relay - them. The format is a URL including a port number - type: str - sample: https://10.0.0.10:8443 - master_flavor_id: - description: - - The flavor of the master node for this ClusterTemplate - type: str - sample: c1.c1r1 - master_lb_enabled: - description: - - Indicates whether created clusters should have a load balancer - for master nodes or not - type: bool - sample: true - name: - description: - - Name that has to be given to the cluster template - type: str - sample: k8scluster - network_driver: - description: - - The name of the driver used for instantiating container networks - type: str - sample: calico - no_proxy: - description: - - A comma separated list of IPs for which proxies should not be - used in the cluster - type: str - sample: 10.0.0.4,10.0.0.5 - public: - description: - - Indicates whether the ClusterTemplate is public or not - type: bool - sample: false - registry_enabled: - description: - - Indicates whether the docker registry is enabled - type: bool - sample: false - server_type: - description: - - Server type for this ClusterTemplate - type: str - sample: vm - tls_disabled: - description: - - Indicates whether the TLS should be disabled - type: bool - sample: false - volume_driver: - description: - - The name of the driver used for instantiating container volumes - type: str - sample: cinder + dns_nameserver: + description: + - The DNS nameserver address. + - Magnum's default value for I(dns_nameserver) is C(8.8.8.8). + type: str + docker_storage_driver: + description: + - Docker storage driver. + choices: [devicemapper, overlay, overlay2] + type: str + docker_volume_size: + description: + - The size in GB of the docker volume. + type: int + external_network_id: + description: + - The external network to attach to the cluster. + - When I(floating_ip_enabled) is set to C(true), then + I(external_network_id) must be defined. + type: str + fixed_network: + description: + - The fixed network name or id to attach to the cluster. + type: str + fixed_subnet: + description: + - The fixed subnet name or id to attach to the cluster. + type: str + flavor_id: + description: + - The flavor of the minion node for this cluster template. + type: str + floating_ip_enabled: + description: + - Indicates whether created clusters should have a floating ip or not. + - When I(floating_ip_enabled) is set to C(true), then + I(external_network_id) must be defined. + type: bool + default: true + keypair_id: + description: + - Name or ID of the keypair to use. + type: str + image_id: + description: + - Image id the cluster will be based on. + - Required if I(state) is C(present). + type: str + labels: + description: + - One or more key/value pairs. + type: raw + http_proxy: + description: + - Address of a proxy that will receive all HTTP requests and relay them. + - The format is a URL including a port number. + type: str + https_proxy: + description: + - Address of a proxy that will receive all HTTPS requests and relay them. + - The format is a URL including a port number. + type: str + master_flavor_id: + description: + - The flavor of the master node for this cluster template. + type: str + master_lb_enabled: + description: + - Indicates whether created clusters should have a load balancer + for master nodes or not. + - Magnum's default value for I(master_lb_enabled) is C(true), + ours is C(false). + type: bool + default: false + name: + description: + - Name that has to be given to the cluster template. + required: true + type: str + network_driver: + description: + - The name of the driver used for instantiating container networks. + choices: [flannel, calico, docker] + type: str + no_proxy: + description: + - A comma separated list of IPs for which proxies should not be + used in the cluster. + type: str + public: + description: + - Indicates whether the cluster template is public or not. + - Magnum's default value for I(public) is C(false). + type: bool + registry_enabled: + description: + - Indicates whether the docker registry is enabled. + - Magnum's default value for I(registry_enabled) is C(false). + type: bool + server_type: + description: + - Server type for this cluster template. + - Magnum's default value for I(server_type) is C(vm). + choices: [vm, bm] + type: str + state: + description: + - Indicate desired state of the resource. + choices: [present, absent] + default: present + type: str + tls_disabled: + description: + - Indicates whether the TLS should be disabled. + - Magnum's default value for I(tls_disabled) is C(false). + type: bool + volume_driver: + description: + - The name of the driver used for instantiating container volumes. + choices: [cinder, rexray] + type: str +notes: + - Return values of this module are preliminary and will most likely change + when openstacksdk has finished its transition of cloud layer functions to + resource proxies. +requirements: + - "python >= 3.6" + - "openstacksdk" +extends_documentation_fragment: + - openstack.cloud.openstack ''' -EXAMPLES = ''' -# Create a new Kubernetes cluster template -- openstack.cloud.coe_cluster_template: - name: k8s +RETURN = r''' +id: + description: The cluster UUID. + returned: On success when I(state) is C(present). + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" +cluster_template: + description: Dictionary describing the template. + returned: On success when I(state) is C(present). + type: complex + contains: + apiserver_port: + description: The exposed port of COE API server. + type: int + cluster_distro: + description: Display the attribute os_distro defined as appropriate + metadata in image for the bay/cluster driver. + type: str + coe: + description: The Container Orchestration Engine for this cluster + template. + type: str + sample: kubernetes + created_at: + description: The date and time when the resource was created. + type: str + dns_nameserver: + description: The DNS nameserver address + type: str + sample: '8.8.8.8' + docker_volume_size: + description: The size in GB of the docker volume + type: int + sample: 5 + external_network_id: + description: The external network to attach to the cluster + type: str + sample: public + fixed_network: + description: The fixed network name to attach to the cluster + type: str + sample: 07767ec6-85f5-44cb-bd63-242a8e7f0d9d + fixed_subnet: + description: The fixed subnet name to attach to the cluster. + type: str + sample: 05567ec6-85f5-44cb-bd63-242a8e7f0d9d + flavor_id: + description: The flavor of the minion node for this cluster template. + type: str + sample: c1.c1r1 + floating_ip_enabled: + description: Indicates whether created clusters should have a + floating ip or not. + type: bool + sample: true + http_proxy: + description: Address of a proxy that will receive all HTTP requests + and relay them. The format is a URL including a port + number. + type: str + sample: http://10.0.0.11:9090 + https_proxy: + description: Address of a proxy that will receive all HTTPS requests + and relay them. The format is a URL including a port + number. + type: str + sample: https://10.0.0.10:8443 + id: + description: The UUID of the cluster template. + type: str + image_id: + description: Image id the cluster will be based on. + type: str + sample: 05567ec6-85f5-44cb-bd63-242a8e7f0e9d + insecure_registry: + description: "The URL pointing to users's own private insecure docker + registry to deploy and run docker containers." + type: str + is_public: + description: Access to a baymodel/cluster template is normally limited to + the admin, owner or users within the same tenant as the + owners. Setting this flag makes the baymodel/cluster + template public and accessible by other users. The default + is not public. + type: bool + is_registry_enabled: + description: "Docker images by default are pulled from the public Docker + registry, but in some cases, users may want to use a + private registry. This option provides an alternative + registry based on the Registry V2: Magnum will create a + local registry in the bay/cluster backed by swift to host + the images. The default is to use the public registry." + type: bool + is_tls_disabled: + description: Transport Layer Security (TLS) is normally enabled to secure + the bay/cluster. In some cases, users may want to disable + TLS in the bay/cluster, for instance during development or + to troubleshoot certain problems. Specifying this parameter + will disable TLS so that users can access the COE endpoints + without a certificate. The default is TLS enabled. + type: bool + keypair_id: + description: Name or ID of the keypair to use. + type: str + sample: mykey + labels: + description: One or more key/value pairs. + type: dict + sample: {'key1': 'value1', 'key2': 'value2'} + location: + description: The OpenStack location of this resource. + type: str + master_flavor_id: + description: The flavor of the master node for this cluster template. + type: str + sample: c1.c1r1 + master_lb_enabled: + description: Indicates whether created clusters should have a load + balancer for master nodes or not. + type: bool + sample: true + name: + description: Name that has to be given to the cluster template. + type: str + sample: k8scluster + network_driver: + description: + - The name of the driver used for instantiating container networks + type: str + sample: calico + no_proxy: + description: A comma separated list of IPs for which proxies should + not be used in the cluster. + type: str + sample: 10.0.0.4,10.0.0.5 + properties: + description: Additional properties of the cluster template. + type: dict + sample: | + { + "docker_storage_driver": null, + "hidden": false, + "master_lb_enabled": false, + "project_id": "8fb245a1bd714d9a82e419f2b7bb69dd", + "tags": null, + "user_id": "51510ce12e294d5d9c7391bececcd1e8" + } + public: + description: Access to a baymodel/cluster template is normally limited + to the admin, owner or users within the same tenant as the + owners. Setting this flag makes the baymodel/cluster + template public and accessible by other users. The default + is not public. + type: bool + sample: false + registry_enabled: + description: "Docker images by default are pulled from the public Docker + registry, but in some cases, users may want to use a + private registry. This option provides an alternative + registry based on the Registry V2: Magnum will create a + local registry in the bay/cluster backed by swift to host + the images. The default is to use the public registry." + type: bool + sample: false + server_type: + description: Server type for this cluster template. + type: str + sample: vm + tls_disabled: + description: Transport Layer Security (TLS) is normally enabled to secure + the bay/cluster. In some cases, users may want to disable + TLS in the bay/cluster, for instance during development or + to troubleshoot certain problems. Specifying this parameter + will disable TLS so that users can access the COE endpoints + without a certificate. The default is TLS enabled. + type: bool + sample: false + updated_at: + description: The date and time when the resource was updated. + type: str + uuid: + description: The UUID of the cluster template. + type: str + volume_driver: + description: The name of the driver used for instantiating container + volumes. + type: str + sample: cinder +''' + +EXAMPLES = r''' +- name: Create a new Kubernetes cluster template + openstack.cloud.coe_cluster_template: + cloud: devstack coe: kubernetes - keypair_id: mykey image_id: 2a8c9888-9054-4b06-a1ca-2bb61f9adb72 + keypair_id: mykey + name: k8s public: no ''' from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule -class CoeClusterTemplateModule(OpenStackModule): +class COEClusterTemplateModule(OpenStackModule): argument_spec = dict( - coe=dict(required=True, choices=['kubernetes', 'swarm', 'mesos']), - dns_nameserver=dict(default='8.8.8.8'), - docker_storage_driver=dict(choices=['devicemapper', 'overlay', 'overlay2']), + coe=dict(choices=['kubernetes', 'swarm', 'mesos']), + dns_nameserver=dict(), + docker_storage_driver=dict(choices=['devicemapper', 'overlay', + 'overlay2']), docker_volume_size=dict(type='int'), - external_network_id=dict(default=None), - fixed_network=dict(default=None), - fixed_subnet=dict(default=None), - flavor_id=dict(default=None), + external_network_id=dict(), + fixed_network=dict(), + fixed_subnet=dict(), + flavor_id=dict(), floating_ip_enabled=dict(type='bool', default=True), - keypair_id=dict(default=None), - image_id=dict(required=True), - labels=dict(default=None, type='raw'), - http_proxy=dict(default=None), - https_proxy=dict(default=None), + http_proxy=dict(), + https_proxy=dict(), + image_id=dict(), + keypair_id=dict(), + labels=dict(type='raw'), + master_flavor_id=dict(), master_lb_enabled=dict(type='bool', default=False), - master_flavor_id=dict(default=None), name=dict(required=True), network_driver=dict(choices=['flannel', 'calico', 'docker']), - no_proxy=dict(default=None), - public=dict(type='bool', default=False), - registry_enabled=dict(type='bool', default=False), - server_type=dict(default="vm", choices=['vm', 'bm']), + no_proxy=dict(), + public=dict(type='bool'), + registry_enabled=dict(type='bool'), + server_type=dict(choices=['vm', 'bm']), state=dict(default='present', choices=['absent', 'present']), - tls_disabled=dict(type='bool', default=False), + tls_disabled=dict(type='bool'), volume_driver=dict(choices=['cinder', 'rexray']), ) - module_kwargs = dict() - - def _parse_labels(self, labels): - if isinstance(labels, str): - labels_dict = {} - for kv_str in labels.split(","): - k, v = kv_str.split("=") - labels_dict[k] = v - return labels_dict - if not labels: - return {} - return labels + module_kwargs = dict( + required_if=[ + ('state', 'present', ('coe', 'image_id')), + ], + supports_check_mode=True, + ) def run(self): - params = self.params.copy() - state = self.params['state'] - name = self.params['name'] - coe = self.params['coe'] - image_id = self.params['image_id'] - kwargs = dict( - dns_nameserver=self.params['dns_nameserver'], - docker_storage_driver=self.params['docker_storage_driver'], - docker_volume_size=self.params['docker_volume_size'], - external_network_id=self.params['external_network_id'], - fixed_network=self.params['fixed_network'], - fixed_subnet=self.params['fixed_subnet'], - flavor_id=self.params['flavor_id'], - floating_ip_enabled=self.params['floating_ip_enabled'], - keypair_id=self.params['keypair_id'], - labels=self._parse_labels(params['labels']), - http_proxy=self.params['http_proxy'], - https_proxy=self.params['https_proxy'], - master_lb_enabled=self.params['master_lb_enabled'], - master_flavor_id=self.params['master_flavor_id'], - network_driver=self.params['network_driver'], - no_proxy=self.params['no_proxy'], - public=self.params['public'], - registry_enabled=self.params['registry_enabled'], - server_type=self.params['server_type'], - tls_disabled=self.params['tls_disabled'], - volume_driver=self.params['volume_driver'], - ) + cluster_template = self._find() - changed = False - template = self.conn.get_coe_cluster_template( - name_or_id=name, filters={'coe': coe, 'image_id': image_id}) + if self.ansible.check_mode: + self.exit_json(changed=self._will_change(state, cluster_template)) - if state == 'present': - if not template: - template = self.conn.create_coe_cluster_template( - name, coe=coe, image_id=image_id, **kwargs) - changed = True - else: - changed = False + if state == 'present' and not cluster_template: + # Create cluster_template + cluster_template = self._create() + self.exit_json( + changed=True, + # for backward compatibility + id=cluster_template['id'], + cluster_template=cluster_template) + + elif state == 'present' and cluster_template: + # Update cluster_template + update = self._build_update(cluster_template) + if update: + cluster_template = self._update(cluster_template, update) self.exit_json( - changed=changed, cluster_template=template, id=template['uuid']) - elif state == 'absent': - if not template: - self.exit_json(changed=False) - else: - self.conn.delete_coe_cluster_template(name) - self.exit_json(changed=True) + changed=bool(update), + # for backward compatibility + id=cluster_template['id'], + cluster_template=cluster_template) + + elif state == 'absent' and cluster_template: + # Delete cluster_template + self._delete(cluster_template) + self.exit_json(changed=True) + + elif state == 'absent' and not cluster_template: + # Do nothing + self.exit_json(changed=False) + + def _build_update(self, cluster_template): + update = {} + + if self.params['floating_ip_enabled'] \ + and self.params['external_network_id'] is None: + raise ValueError('floating_ip_enabled is True' + ' but external_network_id is missing') + + # TODO: Implement support for updates. + non_updateable_keys = [k for k in ['coe', 'dns_nameserver', + 'docker_storage_driver', + 'docker_volume_size', + 'external_network_id', + 'fixed_network', + 'fixed_subnet', 'flavor_id', + 'floating_ip_enabled', + 'http_proxy', 'https_proxy', + 'image_id', 'keypair_id', + 'master_flavor_id', + 'master_lb_enabled', 'name', + 'network_driver', 'no_proxy', + 'public', 'registry_enabled', + 'server_type', 'tls_disabled', + 'volume_driver'] + if self.params[k] is not None + and k in cluster_template + and self.params[k] != cluster_template[k]] + + labels = self.params['labels'] + if labels is not None: + if isinstance(labels, str): + labels = dict([tuple(kv.split(":")) + for kv in labels.split(",")]) + if labels != cluster_template['labels']: + non_updateable_keys.append('labels') + + if non_updateable_keys: + self.fail_json(msg='Cannot update parameters {0}' + .format(non_updateable_keys)) + + attributes = dict((k, self.params[k]) + for k in [] + if self.params[k] is not None + and self.params[k] != cluster_template[k]) + + if attributes: + update['attributes'] = attributes + + return update + + def _create(self): + if self.params['floating_ip_enabled'] \ + and self.params['external_network_id'] is None: + raise ValueError('floating_ip_enabled is True' + ' but external_network_id is missing') + + # TODO: Complement *_id parameters with find_* functions to allow + # specifying names in addition to IDs. + kwargs = dict((k, self.params[k]) + for k in ['coe', 'dns_nameserver', + 'docker_storage_driver', 'docker_volume_size', + 'external_network_id', 'fixed_network', + 'fixed_subnet', 'flavor_id', + 'floating_ip_enabled', 'http_proxy', + 'https_proxy', 'image_id', 'keypair_id', + 'master_flavor_id', 'master_lb_enabled', + 'name', 'network_driver', 'no_proxy', 'public', + 'registry_enabled', 'server_type', + 'tls_disabled', 'volume_driver'] + if self.params[k] is not None) + + labels = self.params['labels'] + if labels is not None: + if isinstance(labels, str): + labels = dict([tuple(kv.split(":")) + for kv in labels.split(",")]) + kwargs['labels'] = labels + + return self.conn.create_cluster_template(**kwargs) + + def _delete(self, cluster_template): + self.conn.delete_cluster_template(cluster_template.name) + + def _find(self): + name = self.params['name'] + filters = {} + + image_id = self.params['image_id'] + if image_id is not None: + filters['image_id'] = image_id + + coe = self.params['coe'] + if coe is not None: + filters['coe'] = coe + + return self.conn.get_cluster_template(name_or_id=name, + filters=filters, + detail=True) + + def _update(self, cluster_template, update): + attributes = update.get('attributes') + if attributes: + # TODO: Implement support for updates. + # cluster_template = self.conn.update_cluster_template(...) + pass + + return cluster_template + + def _will_change(self, state, cluster_template): + if state == 'present' and not cluster_template: + return True + elif state == 'present' and cluster_template: + return bool(self._build_update(cluster_template)) + elif state == 'absent' and cluster_template: + return True + else: + # state == 'absent' and not cluster_template: + return False def main(): - module = CoeClusterTemplateModule() + module = COEClusterTemplateModule() module()