From 4fe73c978ab5cdc4f4f474b43658c09847eae3be Mon Sep 17 00:00:00 2001 From: Rafael Castillo Date: Tue, 1 Nov 2022 13:17:40 -0700 Subject: [PATCH] Updates server_metadata for 2.0.0 - Switch to using proxy layer calls - Return whole server resource instead of only metadata map - Swap the name and server aliased parameters - Add a server_metadata test role - Ensure check mode returns tentatively updated metadata - Remove server_id return value Change-Id: Id63e6a80d1b8e3574911016fec792f00b63f1524 --- .zuul.yaml | 1 + ci/roles/server_metadata/tasks/main.yml | 96 +++++ ci/run-collection.yml | 1 + plugins/modules/server_metadata.py | 446 +++++++++++++++++++----- 4 files changed, 466 insertions(+), 78 deletions(-) create mode 100644 ci/roles/server_metadata/tasks/main.yml diff --git a/.zuul.yaml b/.zuul.yaml index 83bfbca4..bdc07f4a 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -103,6 +103,7 @@ security_group security_group_rule server + server_metadata server_volume stack subnet diff --git a/ci/roles/server_metadata/tasks/main.yml b/ci/roles/server_metadata/tasks/main.yml new file mode 100644 index 00000000..6fe4941b --- /dev/null +++ b/ci/roles/server_metadata/tasks/main.yml @@ -0,0 +1,96 @@ +--- +- name: Create test server + openstack.cloud.server: + cloud: "{{ cloud }}" + state: present + name: ansible_server + image: "{{ image_name }}" + flavor: m1.tiny + network: private + auto_ip: false + wait: true + register: server + +- name: Set server metadata by id + openstack.cloud.server_metadata: + cloud: "{{ cloud }}" + name: "{{ server.server.id }}" + meta: + test_key: test_value + second_key: second_value + register: server_metadata + +- name: Assert updated metadata + assert: + that: + - server_metadata is changed + - "server_metadata.server.metadata == {'test_key': 'test_value', 'second_key': + 'second_value'}" + +- name: Set server metadata by name + openstack.cloud.server_metadata: + cloud: "{{ cloud }}" + name: "{{ server.server.name }}" + meta: + test_key: test_value_2 + register: server_metadata + +- name: Assert updated metadata + assert: + that: + - server_metadata is changed + - "server_metadata.server.metadata == {'test_key': 'test_value_2', 'second_key': + 'second_value'}" + +- name: Update metadata again + openstack.cloud.server_metadata: + cloud: "{{ cloud }}" + name: "{{ server.server.id }}" + meta: + test_key: test_value_2 + register: server_metadata + +- name: Assert not changed + assert: + that: + - server_metadata is not changed + - "server_metadata.server.metadata == {'test_key': 'test_value_2', 'second_key': + 'second_value'}" + +- name: Delete server metadata + openstack.cloud.server_metadata: + cloud: "{{ cloud }}" + name: "{{ server.server.id }}" + state: absent + meta: + test_key: + register: server_metadata + +- name: Assert updated metadata + assert: + that: + - server_metadata is changed + - "server_metadata.server.metadata == {'second_key': 'second_value'}" + +- name: Delete server metadata again + openstack.cloud.server_metadata: + cloud: "{{ cloud }}" + name: "{{ server.server.id }}" + state: absent + meta: + test_key: + register: server_metadata + +- name: Assert not changed + assert: + that: + - server_metadata is not changed + - "server_metadata.server.metadata == {'second_key': 'second_value'}" + +- name: Delete test server + openstack.cloud.server: + cloud: "{{ cloud }}" + name: "{{ server.server.id }}" + state: absent + wait: true + register: server diff --git a/ci/run-collection.yml b/ci/run-collection.yml index ef7086c3..335d7221 100644 --- a/ci/run-collection.yml +++ b/ci/run-collection.yml @@ -58,6 +58,7 @@ - { role: security_group, tags: security_group } - { role: security_group_rule, tags: security_group_rule } - { role: server, tags: server } + - { role: server_metadata, tags: server_metadata } - { role: server_volume, tags: server_volume } - { role: stack, tags: stack } - { role: subnet, tags: subnet } diff --git a/plugins/modules/server_metadata.py b/plugins/modules/server_metadata.py index 11554474..1d5bd933 100644 --- a/plugins/modules/server_metadata.py +++ b/plugins/modules/server_metadata.py @@ -12,18 +12,21 @@ author: OpenStack Ansible SIG description: - Add, Update or Remove metadata in compute instances from OpenStack. options: - server: + name: description: - Name of the instance to update the metadata required: true - aliases: ['name'] + aliases: ['server'] type: str - meta: + metadata: description: - 'A list of key value pairs that should be provided as a metadata to the instance or a string containing a list of key-value pairs. Eg: meta: "key1=value1,key2=value2"' + - Note that when I(state) is C(true), metadata already existing on the + server will not be cleared. required: true + aliases: [meta] type: dict state: description: @@ -41,51 +44,321 @@ extends_documentation_fragment: EXAMPLES = ''' # Creates or updates hostname=test1 as metadata of the server instance vm1 -- name: add metadata to compute instance - hosts: localhost - tasks: - - name: add metadata to instance - openstack.cloud.server_metadata: - state: present - auth: - auth_url: https://openstack-api.example.com:35357/v2.0/ - username: admin - password: admin - project_name: admin - name: vm1 - meta: - hostname: test1 - group: group1 +# Note that existing keys will not be cleared +- name: add metadata to instance + openstack.cloud.server_metadata: + state: present + cloud: "{{ cloud }}" + name: vm1 + metadata: + hostname: test1 + group: group1 # Removes the keys under meta from the instance named vm1 -- name: delete metadata from compute instance - hosts: localhost - tasks: - - name: delete metadata from instance - openstack.cloud.server_metadata: +- name: delete metadata from instance + openstack.cloud.server_metadata: state: absent - auth: - auth_url: https://openstack-api.example.com:35357/v2.0/ - username: admin - password: admin - project_name: admin + cloud: "{{ cloud }}" name: vm1 meta: hostname: group: + public_keys: ''' RETURN = ''' -server_id: - description: The compute instance id where the change was made - returned: success - type: str - sample: "324c4e91-3e03-4f62-9a4d-06119a8a8d16" -metadata: - description: The metadata of compute instance after the change - returned: success +server: + description: Dictionary describing the server that was updated. type: dict - sample: {'key1': 'value1', 'key2': 'value2'} + returned: On success when I(state) is 'present'. + contains: + access_ipv4: + description: | + IPv4 address that should be used to access this server. + May be automatically set by the provider. + returned: success + type: str + access_ipv6: + description: | + IPv6 address that should be used to access this + server. May be automatically set by the provider. + returned: success + type: str + addresses: + description: | + A dictionary of addresses this server can be accessed through. + The dictionary contains keys such as 'private' and 'public', + each containing a list of dictionaries for addresses of that + type. The addresses are contained in a dictionary with keys + 'addr' and 'version', which is either 4 or 6 depending on the + protocol of the IP address. + returned: success + type: dict + admin_password: + description: | + When a server is first created, it provides the administrator + password. + returned: success + type: str + attached_volumes: + description: | + A list of an attached volumes. Each item in the list contains + at least an 'id' key to identify the specific volumes. + returned: success + type: list + availability_zone: + description: | + The name of the availability zone this server is a part of. + returned: success + type: str + block_device_mapping: + description: | + Enables fine grained control of the block device mapping for an + instance. This is typically used for booting servers from + volumes. + returned: success + type: str + compute_host: + description: | + The name of the compute host on which this instance is running. + Appears in the response for administrative users only. + returned: success + type: str + config_drive: + description: | + Indicates whether or not a config drive was used for this + server. + returned: success + type: str + created_at: + description: Timestamp of when the server was created. + returned: success + type: str + description: + description: | + The description of the server. Before microversion + 2.19 this was set to the server name. + returned: success + type: str + disk_config: + description: The disk configuration. Either AUTO or MANUAL. + returned: success + type: str + flavor: + description: The flavor property as returned from server. + returned: success + type: dict + flavor_id: + description: | + The flavor reference, as a ID or full URL, for the flavor to + use for this server. + returned: success + type: str + has_config_drive: + description: | + Indicates whether a configuration drive enables metadata + injection. Not all cloud providers enable this feature. + returned: success + type: str + host_id: + description: An ID representing the host of this server. + returned: success + type: str + host_status: + description: The host status. + returned: success + type: str + hostname: + description: | + The hostname set on the instance when it is booted. + By default, it appears in the response for administrative users + only. + returned: success + type: str + hypervisor_hostname: + description: | + The hypervisor host name. Appears in the response for + administrative users only. + returned: success + type: str + id: + description: ID of the server. + returned: success + type: str + image: + description: The image property as returned from server. + returned: success + type: dict + image_id: + description: | + The image reference, as a ID or full URL, for the image to use + for this server. + returned: success + type: str + instance_name: + description: | + The instance name. The Compute API generates the instance name + from the instance name template. Appears in the response for + administrative users only. + returned: success + type: str + is_locked: + description: The locked status of the server + returned: success + type: bool + kernel_id: + description: | + The UUID of the kernel image when using an AMI. Will be null if + not. By default, it appears in the response for administrative + users only. + returned: success + type: str + key_name: + description: The name of an associated keypair. + returned: success + type: str + launch_index: + description: | + When servers are launched via multiple create, this is the + sequence in which the servers were launched. By default, it + appears in the response for administrative users only. + returned: success + type: int + launched_at: + description: The timestamp when the server was launched. + returned: success + type: str + links: + description: | + A list of dictionaries holding links relevant to this server. + returned: success + type: str + max_count: + description: The maximum number of servers to create. + returned: success + type: str + metadata: + description: List of tag strings. + returned: success + type: dict + min_count: + description: The minimum number of servers to create. + returned: success + type: str + name: + description: Name of the server + returned: success + type: str + networks: + description: | + A networks object. Required parameter when there are multiple + networks defined for the tenant. When you do not specify the + networks parameter, the server attaches to the only network + created for the current tenant. + returned: success + type: str + power_state: + description: The power state of this server. + returned: success + type: str + progress: + description: | + While the server is building, this value represents the + percentage of completion. Once it is completed, it will be 100. + returned: success + type: int + project_id: + description: The ID of the project this server is associated with. + returned: success + type: str + ramdisk_id: + description: | + The UUID of the ramdisk image when using an AMI. Will be null + if not. By default, it appears in the response for + administrative users only. + returned: success + type: str + reservation_id: + description: | + The reservation id for the server. This is an id that can be + useful in tracking groups of servers created with multiple + create, that will all have the same reservation_id. By default, + it appears in the response for administrative users only. + returned: success + type: str + root_device_name: + description: | + The root device name for the instance By default, it appears in + the response for administrative users only. + returned: success + type: str + scheduler_hints: + description: The dictionary of data to send to the scheduler. + returned: success + type: dict + security_groups: + description: | + A list of applicable security groups. Each group contains keys + for: description, name, id, and rules. + returned: success + type: list + elements: dict + server_groups: + description: | + The UUIDs of the server groups to which the server belongs. + Currently this can contain at most one entry. + returned: success + type: list + status: + description: | + The state this server is in. Valid values include 'ACTIVE', + 'BUILDING', 'DELETED', 'ERROR', 'HARD_REBOOT', 'PASSWORD', + 'PAUSED', 'REBOOT', 'REBUILD', 'RESCUED', 'RESIZED', + 'REVERT_RESIZE', 'SHUTOFF', 'SOFT_DELETED', 'STOPPED', + 'SUSPENDED', 'UNKNOWN', or 'VERIFY_RESIZE'. + returned: success + type: str + tags: + description: A list of associated tags. + returned: success + type: list + task_state: + description: The task state of this server. + returned: success + type: str + terminated_at: + description: | + The timestamp when the server was terminated (if it has been). + returned: success + type: str + trusted_image_certificates: + description: | + A list of trusted certificate IDs, that were used during image + signature verification to verify the signing certificate. + returned: success + type: list + updated_at: + description: Timestamp of when this server was last updated. + returned: success + type: str + user_data: + description: | + Configuration information or scripts to use upon launch. + Base64 encoded. + returned: success + type: str + user_id: + description: The ID of the owners of this server. + returned: success + type: str + vm_state: + description: The VM state of this server. + returned: success + type: str + volumes: + description: Same as attached_volumes. + returned: success + type: list ''' from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule @@ -93,62 +366,79 @@ from ansible_collections.openstack.cloud.plugins.module_utils.openstack import O class ServerMetadataModule(OpenStackModule): argument_spec = dict( - server=dict(required=True, aliases=['name']), - meta=dict(required=True, type='dict'), + name=dict(required=True, aliases=['server']), + metadata=dict(required=True, type='dict', aliases=['meta']), state=dict(default='present', choices=['absent', 'present']), ) module_kwargs = dict( supports_check_mode=True ) - def _needs_update(self, server_metadata=None, metadata=None): - if server_metadata is None: - server_metadata = {} - if metadata is None: - metadata = {} - return len(set(metadata.items()) - set(server_metadata.items())) != 0 - - def _get_keys_to_delete(self, server_metadata_keys=None, metadata_keys=None): - if server_metadata_keys is None: - server_metadata_keys = [] - if metadata_keys is None: - metadata_keys = [] - return set(server_metadata_keys) & set(metadata_keys) - def run(self): state = self.params['state'] - server_param = self.params['server'] - meta_param = self.params['meta'] + server_name_or_id = self.params['name'] + metadata = self.params['metadata'] + + server = self.conn.compute.find_server(server_name_or_id, + ignore_missing=False) + # openstacksdk will not return details when looking up by name, so we + # need to refresh the server to get the metadata when updating. + # Can remove when + # https://review.opendev.org/c/openstack/openstacksdk/+/857987 merges + server = self.conn.compute.get_server(server.id) + + if self.ansible.check_mode: + self.exit_json(**self._check_mode_values(state, server, metadata)) + changed = False - - server = self.conn.get_server(server_param) - if not server: - self.fail_json( - msg='Could not find server {0}'.format(server_param)) - if state == 'present': - # check if it needs update - if self._needs_update( - server_metadata=server.metadata, metadata=meta_param - ): - if not self.ansible.check_mode: - self.conn.set_server_metadata(server_param, meta_param) + update = self._build_update(server.metadata, metadata) + if update: + # Pass in all metadata keys to set_server_metadata so server + # object keeps all the keys + new_metadata = (server.metadata or {}) + new_metadata.update(update) + self.conn.compute.set_server_metadata(server, + **new_metadata) changed = True elif state == 'absent': - # remove from params the keys that do not exist in the server - keys_to_delete = self._get_keys_to_delete( - server.metadata.keys(), meta_param.keys()) - if len(keys_to_delete) > 0: - if not self.ansible.check_mode: - self.conn.delete_server_metadata( - server_param, keys_to_delete) + # Only remove keys that exist on the server + keys_to_delete = self._get_keys_to_delete(server.metadata, + metadata) + if keys_to_delete: + self.conn.compute.delete_server_metadata(server, + keys_to_delete) changed = True - if changed: - server = self.conn.get_server(server_param) + self.exit_json(changed=changed, + server=server.to_dict(computed=False)) - self.exit_json( - changed=changed, server_id=server.id, metadata=server.metadata) + def _build_update(self, current=None, requested=None): + current = current or {} + requested = requested or {} + update = dict(requested.items() - current.items()) + return update + + def _get_keys_to_delete(self, current=None, requested=None): + current = current or {} + requested = requested or {} + return set(current.keys() & requested.keys()) + + def _check_mode_values(self, state, server, meta): + "Builds return values for check mode" + changed = False + if state == 'present': + update = self._build_update(server.metadata, meta) + if update: + changed = True + new_metadata = (server.metadata or {}) + new_metadata.update(update) + server.metadata = new_metadata + else: + keys = self._get_keys_to_delete(server.metadata, meta) + for k in keys: + server.meta.pop(k) + return dict(changed=changed, server=server.to_dict(computed=False)) def main():