From ac401bb354c6a1d388848a3ebd18537beba080cc Mon Sep 17 00:00:00 2001 From: Jakob Meng Date: Mon, 11 Jul 2022 08:40:53 +0200 Subject: [PATCH] Refactored server and server_info modules Allow to update server attributes such as its description. Changed default value of server attribute 'security_groups' from ['default'] to [] because the latter is the default in python-openstackclient [1] and the former behavior causes issues with existing servers [2]: Previously, when no 'security_groups' parameter was given, the server module would change existing servers to use the default security group, dropping all other security groups assigned to the server. Our (undocumented) guideline when writing modules is to only add or change what has been requested by the user and to stick to defaults from openstacksdk and python-openstackclient whenever possible. Since we have to break backward compatibility with the next release anyway, we take this opportunity to clean up this odd behavior. Now, when no security groups are given, then security groups of an existing server will not be touched. Closes story #2007893 [2]. Note, Nova will create a server in the default security group, if the security_groups parameter is omitted. Dropped 'openstack' field from server module's results. This variable expanded to additional server information which might be useful for Ansible inventories and was filled from openstacksdk's get_openstack_vars() function [3]. Variables in this function can make additional cloud queries to retrieve additional data, so calling this function can be expensive [4]. Users can use *_info modules to retrieve this data on-demand. Dropped 'availabity_zone' attribute from generic OpenStackModule arguments and inserted it into server and volume modules because it is relevant to those two modules only. This is completes what was started years ago [5] and is possible now since we have breaking changes anyway. Switched attribute name 'userdata' with its alias 'user_data' to match openstacksdk's attribute names which are used e.g. in module results. The previous attribute name 'userdata' is now used as an alias and 'user_data' is used as the attribute name to keep backward compatibility. Wait for server to get into 'ACTIVE' state when creating a server and attribute 'wait' has been set to true. Sorted argument specs and documentation of the server module and marked attributes which are not updatable. Changed unstable bash script example in server module documentation. Renamed server's module attribute 'delete_fip' to 'delete_ips' to match openstacksdk and clarify that it includes all floating ip addresses of the server. Renamed server_info's module attribute 'server' to 'name' and added the former as an alias to be consistent with other *_info modules. Added RETURN fields documentation for the module results of both server and server_info modules. Added description and examples of how to use the 'filters' attribute of the server_info module. Closes story #2007873 [6]. Removed 'openstack_' prefix from module results because the prefix is not consistently used across modules, is more to type without any benefit and removal of the prefix allows us to signal to users that their code for handling module results has to be updated. Many modules have different return values with openstacksdk >= 0.99.0 because it consistently uses resource proxies now. Added assertions for module results to catch future changes in the openstacksdk and our Ansible modules. Added integration tests to check the update mechanism of the server module. Fixed indentation in integration tests. Ensure proper creation and deletion of resources such as networks, subnets and servers in integration tests of server_action module. Renamed ci/roles/server/defaults/main.yaml to main.yml, removing the 'a' in the file extension to be consistent with other filenames. Dropped deprecated function openstack_find_nova_addresses() and incorporated its code directly into the server module because it is not used anywhere else. [1] https://opendev.org/openstack/python-openstackclient/src/commit/e49ad1795b9dd57d5a82fb6f8f365fa20041cf29/openstackclient/compute/v2/server.py#L1070 [2] https://storyboard.openstack.org/#!/story/2007893 [3] https://opendev.org/openstack/openstacksdk/src/commit/9e9fc9879583943a08f854980cca5dfb3a5832f7/openstack/cloud/_compute.py#L1772 [4] https://opendev.org/openstack/openstacksdk/src/commit/9e9fc9879583943a08f854980cca5dfb3a5832f7/openstack/cloud/meta.py#L482 [5] https://github.com/ansible/ansible/commit/9bf33e56dd49e9478ba091a1ba12d17b7caeac24 [6] https://storyboard.openstack.org/#!/story/2007873 Signed-off-by: Jakob Meng Change-Id: I2f955519a7e8c782b1dab8f94f7a019ed384b81d --- .zuul.yaml | 2 +- ci/roles/floating_ip/tasks/main.yml | 60 +- ci/roles/server/defaults/main.yaml | 8 - ci/roles/server/defaults/main.yml | 66 ++ ci/roles/server/tasks/main.yml | 422 +++++-- ci/roles/server/tasks/server_actions.yml | 88 +- plugins/doc_fragments/openstack.py | 4 - plugins/module_utils/openstack.py | 16 - plugins/modules/server.py | 1323 ++++++++++++++-------- plugins/modules/server_info.py | 345 +++++- plugins/modules/server_metadata.py | 5 - plugins/modules/volume.py | 5 + 12 files changed, 1702 insertions(+), 642 deletions(-) delete mode 100644 ci/roles/server/defaults/main.yaml create mode 100644 ci/roles/server/defaults/main.yml diff --git a/.zuul.yaml b/.zuul.yaml index 88f65392..402ab700 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -103,6 +103,7 @@ router security_group security_group_rule + server subnet subnet_pool user @@ -113,7 +114,6 @@ # floating_ip # orchestrate # neutron_rbac - # server - job: name: ansible-collections-openstack-functional-devstack-octavia-base diff --git a/ci/roles/floating_ip/tasks/main.yml b/ci/roles/floating_ip/tasks/main.yml index df324778..ebb41ec7 100644 --- a/ci/roles/floating_ip/tasks/main.yml +++ b/ci/roles/floating_ip/tasks/main.yml @@ -162,11 +162,11 @@ # that no floating ip on public network is associated with "10.7.7.100" before running this role assert: that: - - info.openstack_servers|length == 1 - - info.openstack_servers.0.public_v4|length == 0 - - info.openstack_servers.0.public_v6|length == 0 - - info.openstack_servers.0.addresses.ansible_internal|length == 1 - - info.openstack_servers.0.addresses.ansible_internal|map(attribute="addr")|sort|list == ["10.7.7.100"] + - info.servers|length == 1 + - info.servers.0.public_v4|length == 0 + - info.servers.0.public_v6|length == 0 + - info.servers.0.addresses.ansible_internal|length == 1 + - info.servers.0.addresses.ansible_internal|map(attribute="addr")|sort|list == ["10.7.7.100"] - name: Create server with two nics openstack.cloud.server: @@ -190,11 +190,11 @@ - name: Assert two internal ports and no floating ips on server 2 assert: that: - - info.openstack_servers|length == 1 - - info.openstack_servers.0.public_v4|length == 0 - - info.openstack_servers.0.public_v6|length == 0 - - info.openstack_servers.0.addresses.ansible_internal|length == 2 - - info.openstack_servers.0.addresses.ansible_internal|map(attribute="addr")|sort|list == + - info.servers|length == 1 + - info.servers.0.public_v4|length == 0 + - info.servers.0.public_v6|length == 0 + - info.servers.0.addresses.ansible_internal|length == 2 + - info.servers.0.addresses.ansible_internal|map(attribute="addr")|sort|list == ["10.7.7.101", "10.7.7.102"] # Tests @@ -214,8 +214,8 @@ - name: Assert one internal port and one floating ip on server 1 assert: that: - - info.openstack_servers.0.addresses.ansible_internal|length == 2 - - info.openstack_servers.0.addresses.ansible_internal|map(attribute="OS-EXT-IPS:type")|sort|list == + - info.servers.0.addresses.ansible_internal|length == 2 + - info.servers.0.addresses.ansible_internal|map(attribute="OS-EXT-IPS:type")|sort|list == ["fixed", "floating"] - name: Detach floating IP from server @@ -224,7 +224,7 @@ state: absent server: ansible_server1 network: public - floating_ip_address: "{{ (info.openstack_servers.0.addresses.ansible_internal| + floating_ip_address: "{{ (info.servers.0.addresses.ansible_internal| selectattr('OS-EXT-IPS:type', '==', 'floating')|map(attribute='addr')|list)[0] }}" - name: Get info about server @@ -233,16 +233,16 @@ server: ansible_server1 register: info # When detaching a floating ip from an instance there might be a delay until openstack.cloud.server_info - # does not list it any more in info.openstack_servers.0.addresses.ansible_internal, so retry if necessary. + # does not list it any more in info.servers.0.addresses.ansible_internal, so retry if necessary. retries: 10 delay: 3 - until: info.openstack_servers.0.addresses.ansible_internal|length == 1 + until: info.servers.0.addresses.ansible_internal|length == 1 - name: Assert one internal port on server 1 assert: that: - - info.openstack_servers.0.addresses.ansible_internal|length == 1 - - info.openstack_servers.0.addresses.ansible_internal|map(attribute="addr")|list == ["10.7.7.100"] + - info.servers.0.addresses.ansible_internal|length == 1 + - info.servers.0.addresses.ansible_internal|map(attribute="addr")|list == ["10.7.7.100"] - name: Assign floating IP to server openstack.cloud.floating_ip: @@ -263,8 +263,8 @@ - name: Assert two internal ports and one floating ip on server 2 assert: that: - - info.openstack_servers.0.addresses.ansible_internal|length == 3 - - info.openstack_servers.0.addresses.ansible_internal|map(attribute="OS-EXT-IPS:type")|sort|list == + - info.servers.0.addresses.ansible_internal|length == 3 + - info.servers.0.addresses.ansible_internal|map(attribute="OS-EXT-IPS:type")|sort|list == ["fixed", "fixed", "floating"] - name: Assign a second, specific floating IP to server @@ -288,13 +288,13 @@ # retry because we cannot wait for second floating ip retries: 10 delay: 3 - until: info.openstack_servers.0.addresses.ansible_internal|length == 4 + until: info.servers.0.addresses.ansible_internal|length == 4 - name: Assert two internal ports and two floating ips on server 2 assert: that: - - info.openstack_servers.0.addresses.ansible_internal|length == 4 - - ("10.6.6.150" in info.openstack_servers.0.addresses.ansible_internal|map(attribute="addr")|sort|list) + - info.servers.0.addresses.ansible_internal|length == 4 + - ("10.6.6.150" in info.servers.0.addresses.ansible_internal|map(attribute="addr")|sort|list) - name: Detach second floating IP from server openstack.cloud.floating_ip: @@ -310,15 +310,15 @@ server: ansible_server2 register: info # When detaching a floating ip from an instance there might be a delay until openstack.cloud.server_info - # does not list it any more in info.openstack_servers.0.addresses.ansible_internal, so retry if necessary. + # does not list it any more in info.servers.0.addresses.ansible_internal, so retry if necessary. retries: 10 delay: 3 - until: info.openstack_servers.0.addresses.ansible_internal|length == 3 + until: info.servers.0.addresses.ansible_internal|length == 3 - name: Assert two internal ports and one floating ip on server 2 assert: that: - - info.openstack_servers.0.addresses.ansible_internal|length == 3 + - info.servers.0.addresses.ansible_internal|length == 3 - name: Detach remaining floating IP from server openstack.cloud.floating_ip: @@ -326,7 +326,7 @@ state: absent server: ansible_server2 network: public - floating_ip_address: "{{ (info.openstack_servers.0.addresses.ansible_internal| + floating_ip_address: "{{ (info.servers.0.addresses.ansible_internal| selectattr('OS-EXT-IPS:type', '==', 'floating')|map(attribute='addr')|list)[0] }}" - name: Get info about server @@ -335,16 +335,16 @@ server: ansible_server2 register: info # When detaching a floating ip from an instance there might be a delay until openstack.cloud.server_info - # does not list it any more in info.openstack_servers.0.addresses.ansible_internal, so retry if necessary. + # does not list it any more in info.servers.0.addresses.ansible_internal, so retry if necessary. retries: 10 delay: 3 - until: info.openstack_servers.0.addresses.ansible_internal|length == 2 + until: info.servers.0.addresses.ansible_internal|length == 2 - name: Assert two internal ports on server 2 assert: that: - - info.openstack_servers.0.addresses.ansible_internal|length == 2 - - info.openstack_servers.0.addresses.ansible_internal|map(attribute="addr")|list == ["10.7.7.101", "10.7.7.102"] + - info.servers.0.addresses.ansible_internal|length == 2 + - info.servers.0.addresses.ansible_internal|map(attribute="addr")|list == ["10.7.7.101", "10.7.7.102"] # Clean environment - name: Delete server with two nics diff --git a/ci/roles/server/defaults/main.yaml b/ci/roles/server/defaults/main.yaml deleted file mode 100644 index 0b0f2769..00000000 --- a/ci/roles/server/defaults/main.yaml +++ /dev/null @@ -1,8 +0,0 @@ -server_network: private -server_name: ansible_server -server_alt_network: private_alt -server_alt_subnet: subnet_alt -server_alt_name: ansible_server_alt -flavor: m1.tiny -floating_ip_pool_name: public -boot_volume_size: 5 diff --git a/ci/roles/server/defaults/main.yml b/ci/roles/server/defaults/main.yml new file mode 100644 index 00000000..47063ff8 --- /dev/null +++ b/ci/roles/server/defaults/main.yml @@ -0,0 +1,66 @@ +boot_volume_size: 5 +expected_fields: + - access_ipv4 + - access_ipv6 + - addresses + - admin_password + - attached_volumes + - availability_zone + - block_device_mapping + - compute_host + - config_drive + - created_at + - description + - disk_config + - flavor + - flavor_id + - has_config_drive + - host_id + - host_status + - hostname + - hypervisor_hostname + - id + - image + - image_id + - instance_name + - is_locked + - kernel_id + - key_name + - launch_index + - launched_at + - links + - max_count + - metadata + - min_count + - name + - networks + - power_state + - progress + - project_id + - ramdisk_id + - reservation_id + - root_device_name + - scheduler_hints + - security_groups + - server_groups + - status + - tags + - task_state + - terminated_at + - trusted_image_certificates + - updated_at + - user_data + - user_id + - vm_state + - volumes +flavor: m1.tiny +floating_ip_pool_name: public +server_alt_name: ansible_server_alt +server_alt_network: ansible_server_network_alt +server_alt_security_group: ansible_server_security_group_alt +server_alt_subnet: ansible_server_subnet_alt +server_name: ansible_server +server_network: ansible_server_network +server_port: ansible_server_port +server_security_group: ansible_server_security_group +server_subnet: ansible_server_subnet diff --git a/ci/roles/server/tasks/main.yml b/ci/roles/server/tasks/main.yml index 6b78dd1a..b10b3df4 100644 --- a/ci/roles/server/tasks/main.yml +++ b/ci/roles/server/tasks/main.yml @@ -1,19 +1,79 @@ --- +- name: Create network for server + openstack.cloud.network: + cloud: "{{ cloud }}" + name: "{{ server_network }}" + state: present + register: network + +- name: Create subnet for server + openstack.cloud.subnet: + cidr: 192.168.0.0/24 + cloud: "{{ cloud }}" + name: "{{ server_subnet }}" + network_name: "{{ server_network }}" + state: present + register: subnet + +- name: Create second network for server + openstack.cloud.network: + cloud: "{{ cloud }}" + name: "{{ server_alt_network }}" + state: present + +- name: Create second subnet for server + openstack.cloud.subnet: + cidr: 192.168.1.0/24 + cloud: "{{ cloud }}" + name: "{{ server_alt_subnet }}" + network_name: "{{ server_alt_network }}" + state: present + +- name: Create security group for server + openstack.cloud.security_group: + cloud: "{{ cloud }}" + state: present + name: "{{ server_security_group }}" + register: security_group + +- name: Create second security group for server + openstack.cloud.security_group: + cloud: "{{ cloud }}" + state: present + name: "{{ server_alt_security_group }}" + register: security_group_alt + - name: Create server with meta as CSV openstack.cloud.server: - cloud: "{{ cloud }}" - state: present - name: "{{ server_name }}" - image: "{{ image }}" - flavor: "{{ flavor }}" - network: "{{ server_network }}" - auto_floating_ip: false - meta: "key1=value1,key2=value2" - wait: true + cloud: "{{ cloud }}" + state: present + name: "{{ server_name }}" + image: "{{ image }}" + flavor: "{{ flavor }}" + network: "{{ server_network }}" + auto_ip: false + metadata: "key1=value1,key2=value2" + wait: true register: server - debug: var=server +- name: assert return values of server module + assert: + that: + # allow new fields to be introduced but prevent fields from being removed + - expected_fields|difference(server.server.keys())|length == 0 + +- name: Assert server + assert: + that: + - server.server.name == server_name + - server.server.metadata.keys()|sort == ['key1', 'key2'] + - server.server.metadata['key1'] == 'value1' + - server.server.metadata['key2'] == 'value2' + - server_network in server.server.addresses + - server.server.security_groups|map(attribute='name')|list == ['default'] + - name: Get info about all servers openstack.cloud.server_info: cloud: "{{ cloud }}" @@ -22,14 +82,16 @@ - name: Check info about servers assert: that: - info.openstack_servers|length > 0 + - info.servers|length > 0 + # allow new fields to be introduced but prevent fields from being removed + - expected_fields|difference(info.servers[0].keys())|length == 0 - name: Delete server with meta as CSV openstack.cloud.server: - cloud: "{{ cloud }}" - state: absent - name: "{{ server_name }}" - wait: true + cloud: "{{ cloud }}" + state: absent + name: "{{ server_name }}" + wait: true - name: Get info about all servers openstack.cloud.server_info: @@ -39,21 +101,21 @@ - name: Check info about no servers assert: that: - info.openstack_servers|length == 0 + - info.servers|length == 0 - name: Create server with meta as dict openstack.cloud.server: - cloud: "{{ cloud }}" - state: present - name: "{{ server_name }}" - image: "{{ image }}" - flavor: "{{ flavor }}" - auto_floating_ip: false - network: "{{ server_network }}" - meta: - key1: value1 - key2: value2 - wait: true + cloud: "{{ cloud }}" + state: present + name: "{{ server_name }}" + image: "{{ image }}" + flavor: "{{ flavor }}" + auto_ip: false + network: "{{ server_network }}" + metadata: + key1: value1 + key2: value2 + wait: true register: server - debug: var=server @@ -67,26 +129,66 @@ - name: Check info about server name assert: that: - info.openstack_servers[0].name == "{{ server_name }}" + - info.servers[0].name == "{{ server_name }}" + - info.servers[0].id == server.server.id + +- name: Filter servers + openstack.cloud.server_info: + cloud: "{{ cloud }}" + filters: + id: "{{ server.server.id }}" + metadata: + key1: value1 + key2: value2 + register: info + +- name: Check filter results + assert: + that: info.servers|map(attribute='id')|list == [server.server.id] + +- name: Filter servers with partial data + openstack.cloud.server_info: + cloud: "{{ cloud }}" + filters: + id: "{{ server.server.id }}" + metadata: + key1: value1 + # intentially left out parts of metadata here + register: info + +- name: Check filter results + assert: + that: info.servers|map(attribute='id')|list == [server.server.id] + +- name: Filter servers which should not return results + openstack.cloud.server_info: + cloud: "{{ cloud }}" + filters: + id: "THIS_IS_NOT_A_VALID_ID" + register: info + +- name: Check filter results + assert: + that: info.servers|length == 0 - name: Delete server with meta as dict openstack.cloud.server: - cloud: "{{ cloud }}" - state: absent - name: "{{ server_name }}" - wait: true + cloud: "{{ cloud }}" + state: absent + name: "{{ server_name }}" + wait: true - name: Create server (FIP from pool/network) openstack.cloud.server: - cloud: "{{ cloud }}" - state: present - name: "{{ server_name }}" - image: "{{ image }}" - flavor: "{{ flavor }}" - network: "{{ server_network }}" - floating_ip_pools: - - "{{ floating_ip_pool_name }}" - wait: true + cloud: "{{ cloud }}" + state: present + name: "{{ server_name }}" + image: "{{ image }}" + flavor: "{{ flavor }}" + network: "private" + floating_ip_pools: + - "{{ floating_ip_pool_name }}" + wait: true register: server - debug: var=server @@ -97,52 +199,59 @@ server: "{{ server_name }}" detailed: true register: info + # TODO: Drop ignore_errors once openstacksdk's bug #2010135 has been solved. + # Ref.: https://storyboard.openstack.org/#!/story/2010135 + ignore_errors: yes - name: Check info about server image name assert: that: - info.openstack_servers[0].image.name == "{{ image }}" + - info.servers[0].image.name == "{{ image }}" + # TODO: Drop ignore_errors once openstacksdk's bug #2010135 has been solved. + # Ref.: https://storyboard.openstack.org/#!/story/2010135 + ignore_errors: yes - name: Delete server (FIP from pool/network) openstack.cloud.server: - cloud: "{{ cloud }}" - state: absent - name: "{{ server_name }}" - wait: true + cloud: "{{ cloud }}" + state: absent + name: "{{ server_name }}" + wait: true - name: Create server from volume openstack.cloud.server: - cloud: "{{ cloud }}" - state: present - name: "{{ server_name }}" - image: "{{ image }}" - flavor: "{{ flavor }}" - network: "{{ server_network }}" - auto_floating_ip: false - boot_from_volume: true - volume_size: "{{ boot_volume_size }}" - terminate_volume: true - wait: true + cloud: "{{ cloud }}" + state: present + name: "{{ server_name }}" + image: "{{ image }}" + flavor: "{{ flavor }}" + network: "{{ server_network }}" + auto_ip: false + boot_from_volume: true + volume_size: "{{ boot_volume_size }}" + terminate_volume: true + wait: true register: server - debug: var=server - name: Delete server with volume openstack.cloud.server: - cloud: "{{ cloud }}" - state: absent - name: "{{ server_name }}" - wait: true + cloud: "{{ cloud }}" + state: absent + name: "{{ server_name }}" + wait: true + - name: Create a minimal server openstack.cloud.server: - cloud: "{{ cloud }}" - state: present - name: "{{ server_name }}" - image: "{{ image }}" - flavor: "{{ flavor }}" - network: "{{ server_network }}" - auto_floating_ip: false - wait: true + cloud: "{{ cloud }}" + state: present + name: "{{ server_name }}" + image: "{{ image }}" + flavor: "{{ flavor }}" + network: "{{ server_network }}" + auto_ip: false + wait: true register: server - debug: var=server @@ -155,8 +264,7 @@ - name: Check info about servers in all projects assert: - that: - info.openstack_servers|length > 0 + that: info.servers|length > 0 - name: Get info about one server in all projects openstack.cloud.server_info: @@ -167,14 +275,174 @@ - name: Check info about one server in all projects assert: - that: - info.openstack_servers|length > 0 + that: info.servers|length > 0 - name: Delete minimal server openstack.cloud.server: - cloud: "{{ cloud }}" - state: absent - name: "{{ server_name }}" - wait: true + cloud: "{{ cloud }}" + state: absent + name: "{{ server_name }}" + wait: true -- include_tasks: server_actions.yml +- name: Create port to be attached to server + openstack.cloud.port: + cloud: "{{ cloud }}" + state: present + name: "{{ server_port }}" + network: "{{ server_network }}" + no_security_groups: yes + fixed_ips: + - ip_address: 192.168.0.42 + register: port + +- name: Create server which will be updated + openstack.cloud.server: + auto_ip: false + cloud: "{{ cloud }}" + # TODO: Uncomment once openstacksdk with support for + # description parameter has been released to PyPI. + # Ref.: https://review.opendev.org/c/openstack/openstacksdk/+/850671 + #description: "This is a server" + flavor: "{{ flavor }}" + image: "{{ image }}" + metadata: + key1: value1 + key2: value2 + name: "{{ server_name }}" + nics: + - net-name: 'public' + - net-name: "{{ server_network }}" + - port-id: '{{ port.port.id }}' + state: present + wait: true + register: server + +- debug: var=server + +- name: Update server + openstack.cloud.server: + auto_ip: true + cloud: "{{ cloud }}" + description: "This server got updated" + # flavor cannot be updated but must be present + flavor: "{{ flavor }}" + # image cannot be updated but must be present + image: "{{ image }}" + metadata: + key2: value2 + key3: value3 + name: "{{ server_name }}" + # nics cannot be updated + nics: + - net-name: 'public' + - net-name: "{{ server_network }}" + - port-id: '{{ port.port.id }}' + security_groups: + - '{{ server_security_group }}' + - '{{ server_alt_security_group }}' + state: present + wait: true + register: server_updated + +- debug: var=server_updated + +- name: Assert updated server + assert: + that: + - server.server.id == server_updated.server.id + - server_updated is changed + - server_updated.server.description == "This server got updated" + - "'key1' not in server_updated.server.metadata" + - server_updated.server.metadata['key2'] == 'value2' + - server_updated.server.metadata['key3'] == 'value3' + - server_updated.server.addresses.keys()|sort == [server_network,'public'] + - server_updated.server.addresses[server_network]|length == 2 + - server_updated.server.addresses.public|length > 0 + - port.port.fixed_ips[0].ip_address in + server_updated.server.addresses[server_network]|map(attribute='addr') + - server_updated.server.security_groups|map(attribute='name')|unique|length == 2 + - security_group.secgroup.name in server_updated.server.security_groups|map(attribute='name') + - security_group_alt.secgroup.name in server_updated.server.security_groups|map(attribute='name') + +- name: Update server again + openstack.cloud.server: + auto_ip: true + cloud: "{{ cloud }}" + description: "This server got updated" + # flavor cannot be updated but must be present + flavor: "{{ flavor }}" + # image cannot be updated but must be present + image: "{{ image }}" + metadata: + key2: value2 + key3: value3 + name: "{{ server_name }}" + # nics cannot be updated + nics: + - net-name: 'public' + - net-name: "{{ server_network }}" + - port-id: '{{ port.port.id }}' + security_groups: + - '{{ server_security_group }}' + - '{{ server_alt_security_group }}' + state: present + wait: true + register: server_again + +- name: Assert server did not change + assert: + that: + - server.server.id == server_again.server.id + - server_again is not changed + +- name: Delete updated server + openstack.cloud.server: + cloud: "{{ cloud }}" + delete_ips: yes + name: "{{ server_name }}" + state: absent + wait: true + +- name: Delete port which was attached to server + openstack.cloud.port: + cloud: "{{ cloud }}" + state: absent + name: "{{ server_port }}" + +- name: Delete second security group for server + openstack.cloud.security_group: + cloud: "{{ cloud }}" + state: absent + name: "{{ server_alt_security_group }}" + +- name: Delete security group for server + openstack.cloud.security_group: + cloud: "{{ cloud }}" + state: absent + name: "{{ server_security_group }}" + +- name: Delete second subnet for server + openstack.cloud.subnet: + cloud: "{{ cloud }}" + state: absent + name: "{{ server_alt_subnet }}" + +- name: Delete second network for server + openstack.cloud.network: + cloud: "{{ cloud }}" + state: absent + name: "{{ server_alt_network }}" + +- name: Delete subnet for server + openstack.cloud.subnet: + cloud: "{{ cloud }}" + state: absent + name: "{{ server_subnet }}" + +- name: Delete network for server + openstack.cloud.network: + cloud: "{{ cloud }}" + state: absent + name: "{{ server_network }}" + +- import_tasks: server_actions.yml diff --git a/ci/roles/server/tasks/server_actions.yml b/ci/roles/server/tasks/server_actions.yml index 7d0d33a1..629ed5e2 100644 --- a/ci/roles/server/tasks/server_actions.yml +++ b/ci/roles/server/tasks/server_actions.yml @@ -1,3 +1,20 @@ +--- +- name: Create network for server + openstack.cloud.network: + cloud: "{{ cloud }}" + name: "{{ server_network }}" + state: present + register: network + +- name: Create subnet for server + openstack.cloud.subnet: + cidr: 192.168.0.0/24 + cloud: "{{ cloud }}" + name: "{{ server_subnet }}" + network_name: "{{ server_network }}" + state: present + register: subnet + - name: Create server openstack.cloud.server: cloud: "{{ cloud }}" @@ -19,7 +36,7 @@ - name: Ensure status for server is ACTIVE assert: that: - - info1.openstack_servers.0.status == 'ACTIVE' + - info1.servers.0.status == 'ACTIVE' - name: Stop server openstack.cloud.server_action: @@ -38,7 +55,7 @@ - name: Ensure status for server is SHUTOFF assert: that: - - info2.openstack_servers.0.status == 'SHUTOFF' + - info2.servers.0.status == 'SHUTOFF' - server is changed - name: Stop server again @@ -58,7 +75,7 @@ - name: Ensure status for server is SHUTOFF assert: that: - - info3.openstack_servers.0.status == 'SHUTOFF' + - info3.servers.0.status == 'SHUTOFF' - server is not changed - name: Start server @@ -78,7 +95,7 @@ - name: Ensure status for server is ACTIVE assert: that: - - info4.openstack_servers.0.status == 'ACTIVE' + - info4.servers.0.status == 'ACTIVE' - server is changed - name: Start server again @@ -98,7 +115,7 @@ - name: Ensure status for server is ACTIVE assert: that: - - info5.openstack_servers.0.status == 'ACTIVE' + - info5.servers.0.status == 'ACTIVE' - server is not changed - name: Pause server @@ -118,7 +135,7 @@ - name: Ensure status for server is PAUSED assert: that: - - info6.openstack_servers.0.status == 'PAUSED' + - info6.servers.0.status == 'PAUSED' - server is changed - name: Pause server again @@ -138,7 +155,7 @@ - name: Ensure status for server is PAUSED assert: that: - - info7.openstack_servers.0.status == 'PAUSED' + - info7.servers.0.status == 'PAUSED' - server is not changed - name: Unpause server @@ -158,7 +175,7 @@ - name: Ensure status for server is ACTIVE assert: that: - - info8.openstack_servers.0.status == 'ACTIVE' + - info8.servers.0.status == 'ACTIVE' - server is changed - name: Unpause server again @@ -178,7 +195,7 @@ - name: Ensure status for server is ACTIVE assert: that: - - info9.openstack_servers.0.status == 'ACTIVE' + - info9.servers.0.status == 'ACTIVE' - server is not changed - name: Lock server @@ -198,7 +215,7 @@ - name: Ensure status for server is ACTIVE assert: that: - - info10.openstack_servers.0.status == 'ACTIVE' + - info10.servers.0.status == 'ACTIVE' # not in all versions 'locked' is supported - >- (info10.openstack_server[0]['locked'] is defined and @@ -223,7 +240,7 @@ - name: Ensure status for server is ACTIVE assert: that: - - info11.openstack_servers.0.status == 'ACTIVE' + - info11.servers.0.status == 'ACTIVE' # not in all versions 'locked' is supported - >- (info11.openstack_server[0]['locked'] is defined and @@ -248,7 +265,7 @@ - name: Ensure status for server is ACTIVE assert: that: - - info12.openstack_servers.0.status == 'ACTIVE' + - info12.servers.0.status == 'ACTIVE' # not in all versions 'locked' is supported - >- (info12.openstack_server[0]['locked'] is defined and @@ -273,7 +290,7 @@ - name: Ensure status for server is ACTIVE assert: that: - - info13.openstack_servers.0.status == 'ACTIVE' + - info13.servers.0.status == 'ACTIVE' - server is changed # no support for unlock idempotency # not in all versions 'locked' is supported - >- @@ -298,7 +315,7 @@ - name: Ensure status for server is SUSPENDED assert: that: - - info14.openstack_servers.0.status == 'SUSPENDED' + - info14.servers.0.status == 'SUSPENDED' - server is changed - name: Suspend server again @@ -318,7 +335,7 @@ - name: Ensure status for server is SUSPENDED assert: that: - - info15.openstack_servers.0.status == 'SUSPENDED' + - info15.servers.0.status == 'SUSPENDED' - server is not changed - name: Resume server @@ -338,7 +355,7 @@ - name: Ensure status for server is ACTIVE assert: that: - - info16.openstack_servers.0.status == 'ACTIVE' + - info16.servers.0.status == 'ACTIVE' - server is changed - name: Resume server again @@ -358,7 +375,7 @@ - name: Ensure status for server is ACTIVE assert: that: - - info17.openstack_servers.0.status == 'ACTIVE' + - info17.servers.0.status == 'ACTIVE' - server is not changed - name: Rebuild server - error @@ -394,7 +411,7 @@ - name: Ensure status for server is ACTIVE assert: that: - - info18.openstack_servers.0.status in ('ACTIVE', 'REBUILD') + - info18.servers.0.status in ('ACTIVE', 'REBUILD') - server is changed - name: Rebuild server with admin password @@ -416,7 +433,7 @@ - name: Ensure status for server is ACTIVE assert: that: - - info19.openstack_servers.0.status in ('ACTIVE', 'REBUILD') + - info19.servers.0.status in ('ACTIVE', 'REBUILD') - server is changed - name: Shelve server @@ -436,7 +453,7 @@ - name: Ensure status for server is SHELVED or SHELVED_OFFLOADED assert: that: - - info20.openstack_servers.0.status in ['SHELVED', 'SHELVED_OFFLOADED'] + - info20.servers.0.status in ['SHELVED', 'SHELVED_OFFLOADED'] - server is changed - name: Shelve offload server @@ -457,7 +474,7 @@ # no change if server has been offloaded automatically after first shelve command assert: that: - - info21.openstack_servers.0.status == 'SHELVED_OFFLOADED' + - info21.servers.0.status == 'SHELVED_OFFLOADED' - name: Shelve offload server again openstack.cloud.server_action: @@ -476,7 +493,7 @@ - name: Ensure status for server is SHELVED_OFFLOADED assert: that: - - info22.openstack_servers.0.status == 'SHELVED_OFFLOADED' + - info22.servers.0.status == 'SHELVED_OFFLOADED' - server is not changed - name: Unshelve server @@ -496,7 +513,7 @@ - name: Ensure status for server is ACTIVE assert: that: - - info23.openstack_servers.0.status == 'ACTIVE' + - info23.servers.0.status == 'ACTIVE' - server is changed - name: Unshelve server again @@ -516,9 +533,16 @@ - name: Ensure status for server is ACTIVE assert: that: - - info24.openstack_servers.0.status == 'ACTIVE' + - info24.servers.0.status == 'ACTIVE' - server is not changed +- name: Delete server + openstack.cloud.server: + cloud: "{{ cloud }}" + name: "{{ server_name }}" + state: absent + wait: true + - name: Create network for alternate server openstack.cloud.network: cloud: "{{ cloud_alt }}" @@ -554,7 +578,7 @@ - name: Ensure status for server in alternate project is ACTIVE assert: that: - - info25.openstack_servers.0.status == 'ACTIVE' + - info25.servers.0.status == 'ACTIVE' - name: Try to stop server in alternate project openstack.cloud.server_action: @@ -589,7 +613,7 @@ - name: Ensure status for server is SHUTOFF assert: that: - - info26.openstack_servers.0.status == 'SHUTOFF' + - info26.servers.0.status == 'SHUTOFF' - server_alt is changed - name: Delete server in alternate project @@ -610,3 +634,15 @@ cloud: "{{ cloud_alt }}" name: "{{ server_alt_network }}" state: absent + +- name: Delete subnet for server + openstack.cloud.subnet: + cloud: "{{ cloud }}" + state: absent + name: "{{ server_subnet }}" + +- name: Delete network for server + openstack.cloud.network: + cloud: "{{ cloud }}" + state: absent + name: "{{ server_network }}" diff --git a/plugins/doc_fragments/openstack.py b/plugins/doc_fragments/openstack.py index 6cb13ee5..c8df14a2 100644 --- a/plugins/doc_fragments/openstack.py +++ b/plugins/doc_fragments/openstack.py @@ -86,10 +86,6 @@ options: choices: [ admin, internal, public ] default: public aliases: [ endpoint_type ] - availability_zone: - description: - - Ignored. Present for backwards compatibility - type: str sdk_log_path: description: - Path to the logfile of the OpenStackSDK. If empty no log is written diff --git a/plugins/module_utils/openstack.py b/plugins/module_utils/openstack.py index 609e20fe..b837f555 100644 --- a/plugins/module_utils/openstack.py +++ b/plugins/module_utils/openstack.py @@ -46,7 +46,6 @@ import importlib import os from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.six import iteritems OVERRIDES = {} @@ -73,7 +72,6 @@ def openstack_argument_spec(): login_username=dict(default=OS_USERNAME), auth_url=dict(default=OS_AUTH_URL), region_name=dict(default=OS_REGION_NAME), - availability_zone=dict(), ) if OS_PASSWORD: spec['login_password'] = dict(default=OS_PASSWORD) @@ -86,26 +84,12 @@ def openstack_argument_spec(): return spec -def openstack_find_nova_addresses(addresses, ext_tag, key_name=None): - - ret = [] - for (k, v) in iteritems(addresses): - if key_name and k == key_name: - ret.extend([addrs['addr'] for addrs in v]) - else: - for interface_spec in v: - if 'OS-EXT-IPS:type' in interface_spec and interface_spec['OS-EXT-IPS:type'] == ext_tag: - ret.append(interface_spec['addr']) - return ret - - def openstack_full_argument_spec(**kwargs): spec = dict( cloud=dict(type='raw'), auth_type=dict(), auth=dict(type='dict', no_log=True), region_name=dict(), - availability_zone=dict(), validate_certs=dict(type='bool', aliases=['verify']), ca_cert=dict(aliases=['cacert']), client_cert=dict(aliases=['cert']), diff --git a/plugins/modules/server.py b/plugins/modules/server.py index c3e79943..cd09a8fe 100644 --- a/plugins/modules/server.py +++ b/plugins/modules/server.py @@ -15,184 +15,210 @@ author: OpenStack Ansible SIG description: - Create or Remove compute instances from OpenStack. options: - name: - description: - - Name that has to be given to the instance. It is also possible to - specify the ID of the instance instead of its name if I(state) is I(absent). - required: true - type: str - image: - description: - - The name or id of the base image to boot. - - Required when I(boot_from_volume=true) - type: str - image_exclude: - description: - - Text to use to filter image names, for the case, such as HP, where - there are multiple image names matching the common identifying - portions. image_exclude is a negative match filter - it is text that - may not exist in the image name. - type: str - default: "(deprecated)" - flavor: - description: + auto_ip: + description: + - Ensure instance has public ip however the cloud wants to do that. + type: bool + default: 'yes' + aliases: ['auto_floating_ip', 'public_ip'] + availability_zone: + description: + - Availability zone in which to create the server. + - This server attribute cannot be updated. + type: str + boot_from_volume: + description: + - Should the instance boot from a persistent volume created based on + the image given. Mutually exclusive with boot_volume. + - This server attribute cannot be updated. + type: bool + default: 'no' + boot_volume: + description: + - Volume name or id to use as the volume to boot from. Implies + boot_from_volume. Mutually exclusive with image and boot_from_volume. + - This server attribute cannot be updated. + aliases: ['root_volume'] + type: str + config_drive: + description: + - Whether to boot the server with config drive enabled. + - This server attribute cannot be updated. + type: bool + default: 'no' + delete_ips: + description: + - When I(state) is C(absent) and this option is true, any floating IP + address associated with this server will be deleted along with it. + type: bool + aliases: ['delete_fip'] + default: 'no' + description: + description: + - Description of the server. + type: str + flavor: + description: - The name or id of the flavor in which the new instance has to be created. - Exactly one of I(flavor) and I(flavor_ram) must be defined when I(state=present). - type: str - flavor_ram: - description: - - The minimum amount of ram in MB that the flavor in which the new - instance has to be created must have. - - Exactly one of I(flavor) and I(flavor_ram) must be defined when - I(state=present). - type: int - flavor_include: - description: + - This server attribute cannot be updated. + type: str + flavor_include: + description: - Text to use to filter flavor names, for the case, such as Rackspace, where there are multiple flavors that have the same ram count. flavor_include is a positive match filter - it must exist in the flavor name. - type: str - key_name: - description: - - The key pair name to be used when creating a instance - type: str - security_groups: - description: - - Names of the security groups to which the instance should be - added. This may be a YAML list or a comma separated string. - type: list - default: ['default'] - elements: str - network: - description: + - This server attribute cannot be updated. + type: str + flavor_ram: + description: + - The minimum amount of ram in MB that the flavor in which the new + instance has to be created must have. + - Exactly one of I(flavor) and I(flavor_ram) must be defined when + I(state=present). + - This server attribute cannot be updated. + type: int + floating_ip_pools: + description: + - Name of floating IP pool from which to choose a floating IP. + type: list + elements: str + floating_ips: + description: + - list of valid floating IPs that pre-exist to assign to this node. + type: list + elements: str + image: + description: + - The name or id of the base image to boot. + - Required when I(boot_from_volume=true). + - This server attribute cannot be updated. + type: str + image_exclude: + description: + - Text to use to filter image names, for the case, such as HP, where + there are multiple image names matching the common identifying + portions. image_exclude is a negative match filter - it is text that + may not exist in the image name. + - This server attribute cannot be updated. + type: str + default: "(deprecated)" + key_name: + description: + - The key pair name to be used when creating a instance. + - This server attribute cannot be updated. + type: str + metadata: + description: + - 'A list of key value pairs that should be provided as a metadata to + the new instance or a string containing a list of key-value pairs. + Example: metadata: "key1=value1,key2=value2"' + aliases: ['meta'] + type: raw + name: + description: + - Name that has to be given to the instance. It is also possible to + specify the ID of the instance instead of its name if I(state) is + I(absent). + - This server attribute cannot be updated. + required: true + type: str + network: + description: - Name or ID of a network to attach this instance to. A simpler - version of the nics parameter, only one of network or nics should - be supplied. - type: str - nics: - description: + version of the I(nics) parameter, only one of I(network) or I(nics) + should be supplied. + - This server attribute cannot be updated. + type: str + nics: + description: - A list of networks to which the instance's interface should be attached. Networks may be referenced by net-id/net-name/port-id or port-name. - 'Also this accepts a string containing a list of (net/port)-(id/name) - Eg: nics: "net-id=uuid-1,port-name=myport" - Only one of network or nics should be supplied.' - type: list - elements: raw - suboptions: - tag: - description: - - 'A "tag" for the specific port to be passed via metadata. - Eg: tag: test_tag' - auto_ip: - description: - - Ensure instance has public ip however the cloud wants to do that - type: bool - default: 'yes' - aliases: ['auto_floating_ip', 'public_ip'] - floating_ips: - description: - - list of valid floating IPs that pre-exist to assign to this node - type: list - elements: str - floating_ip_pools: - description: - - Name of floating IP pool from which to choose a floating IP - type: list - elements: str - meta: - description: - - 'A list of key value pairs that should be provided as a metadata to - the new instance or a string containing a list of key-value pairs. - Eg: meta: "key1=value1,key2=value2"' - type: raw - wait: - description: - - If the module should wait for the instance to be created. - type: bool - default: 'yes' - timeout: - description: + Example: C(nics: "net-id=uuid-1,port-name=myport")' + - Only one of I(network) or I(nics) should be supplied. + - This server attribute cannot be updated. + type: list + elements: raw + suboptions: + tag: + description: + - 'A I(tag) for the specific port to be passed via metadata. + Eg: C(tag: test_tag)' + reuse_ips: + description: + - When I(auto_ip) is true and this option is true, the I(auto_ip) code + will attempt to re-use unassigned floating ips in the project before + creating a new one. It is important to note that it is impossible + to safely do this concurrently, so if your use case involves + concurrent server creation, it is highly recommended to set this to + false and to delete the floating ip associated with a server when + the server is deleted using I(delete_ips). + - This server attribute cannot be updated. + type: bool + default: 'yes' + scheduler_hints: + description: + - Arbitrary key/value pairs to the scheduler for custom use. + - This server attribute cannot be updated. + type: dict + security_groups: + description: + - Names or IDs of the security groups to which the instance should be + added. + - On server creation, if I(security_groups) is omitted, the API creates + the server in the default security group. + - Requested security groups are not applied to pre-existing ports. + type: list + elements: str + default: [] + state: + description: + - Should the resource be C(present) or C(absent). + choices: [present, absent] + default: present + type: str + terminate_volume: + description: + - If C(yes), delete volume when deleting the instance and if it has + been booted from volume(s). + - This server attribute cannot be updated. + type: bool + default: 'no' + timeout: + description: - The amount of time the module should wait for the instance to get into active state. - default: 180 - type: int - config_drive: - description: - - Whether to boot the server with config drive enabled - type: bool - default: 'no' - userdata: - description: - - Opaque blob of data which is made available to the instance - type: str - aliases: ['user_data'] - boot_from_volume: - description: - - Should the instance boot from a persistent volume created based on - the image given. Mutually exclusive with boot_volume. - type: bool - default: 'no' - volume_size: - description: + default: 180 + type: int + user_data: + description: + - Opaque blob of data which is made available to the instance. + - This server attribute cannot be updated. + type: str + aliases: ['userdata'] + volume_size: + description: - The size of the volume to create in GB if booting from volume based on an image. - type: int - boot_volume: - description: - - Volume name or id to use as the volume to boot from. Implies - boot_from_volume. Mutually exclusive with image and boot_from_volume. - aliases: ['root_volume'] - type: str - terminate_volume: - description: - - If C(yes), delete volume when deleting instance (if booted from volume) - type: bool - default: 'no' - volumes: - description: - - A list of preexisting volumes names or ids to attach to the instance - default: [] - type: list - elements: str - scheduler_hints: - description: - - Arbitrary key/value pairs to the scheduler for custom use - type: dict - state: - description: - - Should the resource be present or absent. - choices: [present, absent] - default: present - type: str - delete_fip: - description: - - When I(state) is absent and this option is true, any floating IP - associated with the instance will be deleted along with the instance. - type: bool - default: 'no' - reuse_ips: - description: - - When I(auto_ip) is true and this option is true, the I(auto_ip) code - will attempt to re-use unassigned floating ips in the project before - creating a new one. It is important to note that it is impossible - to safely do this concurrently, so if your use case involves - concurrent server creation, it is highly recommended to set this to - false and to delete the floating ip associated with a server when - the server is deleted using I(delete_fip). - type: bool - default: 'yes' - availability_zone: - description: - - Availability zone in which to create the server. - type: str - description: - description: - - Description of the server. - type: str + - This server attribute cannot be updated. + type: int + volumes: + description: + - A list of preexisting volumes names or ids to attach to the instance + - This server attribute cannot be updated. + default: [] + type: list + elements: str + wait: + description: + - If the module should wait for the instance to be created. + type: bool + default: 'yes' requirements: - "python >= 3.6" - "openstacksdk" @@ -202,7 +228,7 @@ extends_documentation_fragment: ''' EXAMPLES = ''' -- name: Create a new instance and attaches to a network and passes metadata to the instance +- name: Create a new instance with metadata and attaches it to a network openstack.cloud.server: state: present auth: @@ -242,7 +268,8 @@ EXAMPLES = ''' key_name: test timeout: 200 flavor: 101 - security_groups: default + security_groups: + - default auto_ip: yes # Create a new instance in named cloud mordred availability zone az2 @@ -307,9 +334,11 @@ EXAMPLES = ''' key_name: ansible_key timeout: 200 flavor: 4 - nics: "net-id=4cb08b20-62fe-11e5-9d70-feff819cdc9f,net-id=542f0430-62fe-11e5-9d70-feff819cdc9f..." + nics: >- + net-id=4cb08b20-62fe-11e5-9d70-feff819cdc9f, + net-id=542f0430-62fe-11e5-9d70-feff819cdc9f -- name: Creates a new instance and attaches to a network and passes metadata to the instance +- name: Creates a new instance with metadata and attaches it to a network openstack.cloud.server: state: present auth: @@ -384,7 +413,7 @@ EXAMPLES = ''' image: "Ubuntu Server 14.04" flavor: "P-1" network: "Production" - userdata: | + user_data: | #cloud-config chpasswd: list: | @@ -402,15 +431,13 @@ EXAMPLES = ''' openstack.cloud.server: name: vm1 state: present - image: "Ubuntu Server 14.04" + image: "Ubuntu Server 22.04" flavor: "P-1" network: "Production" - userdata: | - {%- raw -%}#!/bin/bash - echo " up ip route add 10.0.0.0/8 via {% endraw -%}{{ intra_router }}{%- raw -%}" >> /etc/network/interfaces.d/eth0.conf - echo " down ip route del 10.0.0.0/8" >> /etc/network/interfaces.d/eth0.conf - ifdown eth0 && ifup eth0 - {% endraw %} + user_data: | + #!/bin/sh + apt update + apt -y full-upgrade # Create a new instance with server group for (anti-)affinity # server group ID is returned from openstack.cloud.server_group module. @@ -455,66 +482,339 @@ EXAMPLES = ''' ''' -from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( - openstack_find_nova_addresses, OpenStackModule) - - -def _parse_nics(nics): - for net in nics: - if isinstance(net, str): - for nic in net.split(','): - yield dict((nic.split('='),)) - else: - yield net - - -def _parse_meta(meta): - if isinstance(meta, str): - metas = {} - for kv_str in meta.split(","): - k, v = kv_str.split("=") - metas[k] = v - return metas - if not meta: - return {} - return meta +RETURN = ''' +server: + description: Dictionary describing the server. + type: dict + 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 class ServerModule(OpenStackModule): argument_spec = dict( - name=dict(required=True), - image=dict(), - image_exclude=dict(default='(deprecated)'), - flavor=dict(), - flavor_ram=dict(type='int'), - flavor_include=dict(), - key_name=dict(), - security_groups=dict(default=['default'], type='list', elements='str'), - network=dict(), - nics=dict(default=[], type='list', elements='raw'), - meta=dict(type='raw'), - userdata=dict(aliases=['user_data']), - config_drive=dict(default=False, type='bool'), - auto_ip=dict(default=True, type='bool', aliases=['auto_floating_ip', 'public_ip']), - floating_ips=dict(type='list', elements='str'), - floating_ip_pools=dict(type='list', elements='str'), - volume_size=dict(type='int'), + auto_ip=dict(default=True, type='bool', + aliases=['auto_floating_ip', 'public_ip']), + availability_zone=dict(), boot_from_volume=dict(default=False, type='bool'), boot_volume=dict(aliases=['root_volume']), - terminate_volume=dict(default=False, type='bool'), - volumes=dict(default=[], type='list', elements='str'), - scheduler_hints=dict(type='dict'), - state=dict(default='present', choices=['absent', 'present']), - delete_fip=dict(default=False, type='bool'), - reuse_ips=dict(default=True, type='bool'), + config_drive=dict(default=False, type='bool'), + delete_ips=dict(default=False, type='bool', aliases=['delete_fip']), description=dict(), + flavor=dict(), + flavor_include=dict(), + flavor_ram=dict(type='int'), + floating_ip_pools=dict(type='list', elements='str'), + floating_ips=dict(type='list', elements='str'), + image=dict(), + image_exclude=dict(default='(deprecated)'), + key_name=dict(), + metadata=dict(type='raw', aliases=['meta']), + name=dict(required=True), + network=dict(), + nics=dict(default=[], type='list', elements='raw'), + reuse_ips=dict(default=True, type='bool'), + scheduler_hints=dict(type='dict'), + security_groups=dict(default=[], type='list', elements='str'), + state=dict(default='present', choices=['absent', 'present']), + terminate_volume=dict(default=False, type='bool'), + user_data=dict(aliases=['userdata']), + volume_size=dict(type='int'), + volumes=dict(default=[], type='list', elements='str'), ) + module_kwargs = dict( mutually_exclusive=[ - ['auto_ip', 'floating_ips'], - ['auto_ip', 'floating_ip_pools'], - ['floating_ips', 'floating_ip_pools'], + ['auto_ip', 'floating_ips', 'floating_ip_pools'], ['flavor', 'flavor_ram'], ['image', 'boot_volume'], ['boot_from_volume', 'boot_volume'], @@ -522,277 +822,394 @@ class ServerModule(OpenStackModule): ], required_if=[ ('boot_from_volume', True, ['volume_size', 'image']), + ('state', 'present', ('image', 'boot_volume'), True), + ('state', 'present', ('flavor', 'flavor_ram'), True), ], + supports_check_mode=True, ) def run(self): - state = self.params['state'] - image = self.params['image'] - boot_volume = self.params['boot_volume'] - flavor = self.params['flavor'] - flavor_ram = self.params['flavor_ram'] - if state == 'present': - if not (image or boot_volume): - self.fail( - msg="Parameter 'image' or 'boot_volume' is required " - "if state == 'present'" - ) - if not flavor and not flavor_ram: - self.fail( - msg="Parameter 'flavor' or 'flavor_ram' is required " - "if state == 'present'" - ) - - if state == 'present': - self._get_server_state() - self._create_server() - elif state == 'absent': - self._get_server_state() - self._delete_server() - - def _exit_hostvars(self, server, changed=True): - hostvars = self.conn.get_openstack_vars(server) - self.exit( - changed=changed, server=server, id=server.id, openstack=hostvars) - - def _get_server_state(self): - state = self.params['state'] + # self.conn.get_server is required for server.addresses and + # server.interface_ip which self.conn.compute.find_server + # does not return server = self.conn.get_server(self.params['name']) - if server and state == 'present': - if server.status not in ('ACTIVE', 'SHUTOFF', 'PAUSED', 'SUSPENDED'): - self.fail( - msg="The instance is available but not Active state: " + server.status) - (ip_changed, server) = self._check_ips(server) - (sg_changed, server) = self._check_security_groups(server) - (server_changed, server) = self._update_server(server) - self._exit_hostvars(server, ip_changed or sg_changed or server_changed) - if server and state == 'absent': - return True - if state == 'absent': - self.exit(changed=False, result="not present") - return True - def _create_server(self): - flavor = self.params['flavor'] - flavor_ram = self.params['flavor_ram'] - flavor_include = self.params['flavor_include'] + if self.ansible.check_mode: + self.exit_json(changed=self._will_change(state, server)) + + if state == 'present' and not server: + # Create server + server = self._create() + self.exit_json(changed=True, + server=server.to_dict(computed=False)) + + elif state == 'present' and server: + # Update server + update = self._build_update(server) + if update: + server = self._update(server, update) + else: + # drop attributes added in function + # openstacksdk.meta.add_server_interfaces() + # because all other branches do not return them + # + # addresses will be expanded by get_server's call to + # openstacksdk.meta.add_server_interfaces() but we + # cannot easily undo this so we ignore it + for k in ['public_v4', 'public_v6', 'interface_ip']: + del server[k] + + self.exit_json(changed=bool(update), + server=server.to_dict(computed=False)) + + elif state == 'absent' and server: + # Delete server + self._delete(server) + self.exit_json(changed=True) + + elif state == 'absent' and not server: + # Do nothing + self.exit_json(changed=False) + + def _build_update(self, server): + if server.status not in ('ACTIVE', 'SHUTOFF', 'PAUSED', 'SUSPENDED'): + self.fail_json(msg="The instance is available but not " + "active state: {0}".format(server.status)) + + return { + **self._build_update_ips(server), + **self._build_update_security_groups(server), + **self._build_update_server(server)} + + def _build_update_ips(self, server): + auto_ip = self.params['auto_ip'] + floating_ips = self.params['floating_ips'] + floating_ip_pools = self.params['floating_ip_pools'] + + if not (auto_ip or floating_ips or floating_ip_pools): + # No floating ip has been requested, so + # do not add or remove any floating ip. + return {} + + if (auto_ip and server['interface_ip'] + and not (floating_ip_pools or floating_ips)): + # Server has a floating ip address attached and + # no specific floating ip has been requested, + # so nothing to change. + return {} + + # Get floating ip addresses attached to the server + ips = [interface_spec['addr'] + for v in server.addresses.values() + for interface_spec in v + if ('OS-EXT-IPS:type' in interface_spec + and interface_spec['OS-EXT-IPS:type'] == 'floating')] + + if not ips: + # One or multiple floating ips have been requested, + # but none have been attached, so attach them. + return dict(ips=dict( + auto_ip=auto_ip, + ips=floating_ips, + ip_pool=floating_ip_pools)) + + if auto_ip or not floating_ips: + # Nothing do to because either any floating ip address + # or no specific floating ips have been requested + # and any floating ip has been attached. + return {} + + # A specific set of floating ips has been requested + update = {} + add_ips = [ip for ip in floating_ips if ip not in ips] + if add_ips: + # add specific ips which have not been added + update['add_ips'] = add_ips + + remove_ips = [ip for ip in ips if ip not in floating_ips] + if remove_ips: + # Detach ips which are not supposed to be attached + update['remove_ips'] = remove_ips + + def _build_update_security_groups(self, server): + update = {} + + required_security_groups = dict( + (sg['id'], sg) for sg in [ + self.conn.network.find_security_group( + security_group_name_or_id, ignore_missing=False) + for security_group_name_or_id in self.params['security_groups'] + ]) + + # Retrieve IDs of security groups attached to the server + server = self.conn.compute.fetch_server_security_groups(server) + assigned_security_groups = dict( + (sg['id'], self.conn.network.get_security_group(sg['id'])) + for sg in server.security_groups) + + # openstacksdk adds security groups to server using resources + add_security_groups = [ + sg for (sg_id, sg) in required_security_groups.items() + if sg_id not in assigned_security_groups] + + if add_security_groups: + update['add_security_groups'] = add_security_groups + + # openstacksdk removes security groups from servers using resources + remove_security_groups = [ + sg for (sg_id, sg) in assigned_security_groups.items() + if sg_id not in required_security_groups] + + if remove_security_groups: + update['remove_security_groups'] = remove_security_groups + + return update + + def _build_update_server(self, server): + update = {} + + # Process metadata + required_metadata = self._parse_metadata(self.params['metadata']) + assigned_metadata = server.metadata + + add_metadata = dict() + for (k, v) in required_metadata.items(): + if k not in assigned_metadata or assigned_metadata[k] != v: + add_metadata[k] = v + + if add_metadata: + update['add_metadata'] = add_metadata + + remove_metadata = dict() + for (k, v) in assigned_metadata.items(): + if k not in required_metadata or required_metadata[k] != v: + remove_metadata[k] = v + + if remove_metadata: + update['remove_metadata'] = remove_metadata + + # Process server attributes + + # Updateable server attributes in openstacksdk + # (OpenStack API names in braces): + # - access_ipv4 (accessIPv4) + # - access_ipv6 (accessIPv6) + # - name (name) + # - hostname (hostname) + # - disk_config (OS-DCF:diskConfig) + # - description (description) + # Ref.: https://docs.openstack.org/api-ref/compute/#update-server + + # A server's name cannot be updated by this module because + # it is used to find servers by name or id. + # If name is an id, then we do not have a name to update. + # If name is a name actually, then it was used to find a + # matching server hence the name is the user defined one + # already. + + # Update all known updateable attributes although + # our module might not support them yet + server_attributes = dict( + (k, self.params[k]) + for k in ['access_ipv4', 'access_ipv6', 'hostname', 'disk_config', + 'description'] + if k in self.params and self.params[k] is not None + and self.params[k] != server[k]) + + if server_attributes: + update['server_attributes'] = server_attributes + + return update + + def _create(self): + flavor_name_or_id = self.params['flavor'] image_id = None if not self.params['boot_volume']: image_id = self.conn.get_image_id( self.params['image'], self.params['image_exclude']) if not image_id: - self.fail( - msg="Could not find image %s" % self.params['image']) + self.fail_json( + msg="Could not find image {0} with exclude {1}".format( + self.params['image'], self.params['image_exclude'])) - if flavor: - flavor_dict = self.conn.get_flavor(flavor) - if not flavor_dict: - self.fail(msg="Could not find flavor %s" % flavor) + if flavor_name_or_id: + flavor = self.conn.compute.find_flavor(flavor_name_or_id, + ignore_missing=False) else: - flavor_dict = self.conn.get_flavor_by_ram(flavor_ram, flavor_include) - if not flavor_dict: - self.fail(msg="Could not find any matching flavor") + flavor = self.conn.get_flavor_by_ram(self.params['flavor_ram'], + self.params['flavor_include']) + if not flavor: + self.fail_json(msg="Could not find any matching flavor") - nics = self._network_args() - - self.params['meta'] = _parse_meta(self.params['meta']) - - bootkwargs = self.check_versioned( - name=self.params['name'], + args = dict( + flavor=flavor.id, image=image_id, - flavor=flavor_dict['id'], - nics=nics, - meta=self.params['meta'], - security_groups=self.params['security_groups'], - userdata=self.params['userdata'], - config_drive=self.params['config_drive'], - ) - for optional_param in ( - 'key_name', 'availability_zone', 'network', - 'scheduler_hints', 'volume_size', 'volumes', - 'description'): - if self.params[optional_param]: - bootkwargs[optional_param] = self.params[optional_param] - - server = self.conn.create_server( ip_pool=self.params['floating_ip_pools'], ips=self.params['floating_ips'], - auto_ip=self.params['auto_ip'], - boot_volume=self.params['boot_volume'], - boot_from_volume=self.params['boot_from_volume'], - terminate_volume=self.params['terminate_volume'], - reuse_ips=self.params['reuse_ips'], - wait=self.params['wait'], timeout=self.params['timeout'], - **bootkwargs + meta=self._parse_metadata(self.params['metadata']), + nics=self._parse_nics(), ) - self._exit_hostvars(server) + for k in ['auto_ip', 'availability_zone', 'boot_from_volume', + 'boot_volume', 'config_drive', 'description', 'key_name', + 'name', 'network', 'reuse_ips', 'scheduler_hints', + 'security_groups', 'terminate_volume', 'timeout', + 'user_data', 'volume_size', 'volumes', 'wait']: + if self.params[k] is not None: + args[k] = self.params[k] - def _update_server(self, server): - changed = False + server = self.conn.create_server(**args) - self.params['meta'] = _parse_meta(self.params['meta']) + return server - # self.conn.set_server_metadata only updates the key=value pairs, it doesn't - # touch existing ones - update_meta = {} - for (k, v) in self.params['meta'].items(): - if k not in server.metadata or server.metadata[k] != v: - update_meta[k] = v + def _delete(self, server): + self.conn.delete_server( + server.id, + **dict((k, self.params[k]) + for k in ['wait', 'timeout', 'delete_ips'])) - if update_meta: - self.conn.set_server_metadata(server, update_meta) - changed = True - # Refresh server vars - server = self.conn.get_server(self.params['name']) + def _update(self, server, update): + server = self._update_ips(server, update) + server = self._update_security_groups(server, update) + server = self._update_server(server, update) + # Refresh server attributes after security groups etc. have changed + # + # self.conn.get_server() is unnecessary because server.addresses and + # server.interface_ip are computed and hence not returned anyway + return self.conn.compute.find_server(name_or_id=server.id) - return (changed, server) + def _update_ips(self, server, update): + args = dict((k, self.params[k]) for k in ['wait', 'timeout']) + ips = update.get('ips') + if ips: + return self.conn.add_ips_to_server(server, **ips, **args) - def _delete_server(self): - try: - self.conn.delete_server( - self.params['name'], wait=self.params['wait'], - timeout=self.params['timeout'], - delete_ips=self.params['delete_fip']) - except Exception as e: - self.fail(msg="Error in deleting vm: %s" % e) - self.exit(changed=True, result='deleted') + add_ips = update.get('add_ips') + if add_ips: + # Add specific ips which have not been added + server = self.conn.add_ip_list(server, add_ips, **args) - def _network_args(self): - args = [] - nics = self.params['nics'] + remove_ips = update.get('remove_ips') + if remove_ips: + # Detach ips which are not supposed to be attached + for ip in remove_ips: + ip_id = self.conn.network.find_ip(name_or_id=ip, + ignore_missing=False).id + # self.network.update_ip(ip_id, port_id=None) would not handle + # nova network which self.conn.detach_ip_from_server() does + self.conn.detach_ip_from_server(server_id=server.id, + floating_ip_id=ip_id) + return server - if not isinstance(nics, list): - self.fail(msg='The \'nics\' parameter must be a list.') + def _update_security_groups(self, server, update): + add_security_groups = update.get('add_security_groups') + if add_security_groups: + for sg in add_security_groups: + self.conn.compute.add_security_group_to_server(server, sg) - for num, net in enumerate(_parse_nics(nics)): + remove_security_groups = update.get('remove_security_groups') + if remove_security_groups: + for sg in remove_security_groups: + self.conn.compute.remove_security_group_from_server(server, sg) + + # Whenever security groups of a server have changed, + # the server object has to be refreshed. This will + # be postponed until all updates have been applied. + return server + + def _update_server(self, server, update): + add_metadata = update.get('add_metadata') + if add_metadata: + self.conn.compute.set_server_metadata(server.id, + **add_metadata) + + remove_metadata = update.get('remove_metadata') + if remove_metadata: + self.conn.compute.delete_server_metadata(server.id, + remove_metadata.keys()) + + server_attributes = update.get('server_attributes') + if server_attributes: + # Server object cannot passed to self.conn.compute.update_server() + # entirely because its security_groups attribute was expanded by + # self.conn.compute.fetch_server_security_groups() previously which + # thus will no longer have a valid value for OpenStack API. + server = self.conn.compute.update_server(server['id'], + **server_attributes) + + # Whenever server attributes such as metadata have changed, + # the server object has to be refreshed. This will + # be postponed until all updates have been applied. + return server + + def _parse_metadata(self, metadata): + if not metadata: + return {} + + if isinstance(metadata, str): + metas = {} + for kv_str in metadata.split(","): + k, v = kv_str.split("=") + metas[k] = v + return metas + + return metadata + + def _parse_nics(self): + nics = [] + stringified_nets = self.params['nics'] + + if not isinstance(stringified_nets, list): + self.fail_json(msg="The 'nics' parameter must be a list.") + + nets = [(dict((nested_net.split('='),)) + for nested_net in net.split(',')) + if isinstance(net, str) else net + for net in stringified_nets] + + for net in nets: if not isinstance(net, dict): - self.fail( - msg='Each entry in the \'nics\' parameter must be a dict.') + self.fail_json( + msg="Each entry in the 'nics' parameter must be a dict.") if net.get('net-id'): - args.append(net) + nics.append(net) elif net.get('net-name'): - by_name = self.conn.get_network(net['net-name']) - if not by_name: - self.fail( - msg='Could not find network by net-name: %s' % - net['net-name']) - resolved_net = net.copy() - del resolved_net['net-name'] - resolved_net['net-id'] = by_name['id'] - args.append(resolved_net) + network_id = self.conn.network.find_network( + net['net-name'], ignore_missing=False).id + # Replace net-name with net-id and keep optional nic args + # Ref.: https://github.com/ansible/ansible/pull/20969 + del net['net-name'] + net['net-id'] = network_id + nics.append(net) elif net.get('port-id'): - args.append(net) + nics.append(net) elif net.get('port-name'): - by_name = self.conn.get_port(net['port-name']) - if not by_name: - self.fail( - msg='Could not find port by port-name: %s' % - net['port-name']) - resolved_net = net.copy() - del resolved_net['port-name'] - resolved_net['port-id'] = by_name['id'] - args.append(resolved_net) + port_id = self.conn.network.find_port( + net['port-name'], ignore_missing=False).id + # Replace net-name with net-id and keep optional nic args + # Ref.: https://github.com/ansible/ansible/pull/20969 + del net['port-name'] + net['port-id'] = port_id + nics.append(net) if 'tag' in net: - args[num]['tag'] = net['tag'] - return args + nics[-1]['tag'] = net['tag'] + return nics - def _detach_ip_list(self, server, extra_ips): - for ip in extra_ips: - ip_id = self.conn.get_floating_ip( - id=None, filters={'floating_ip_address': ip}) - self.conn.detach_ip_from_server( - server_id=server.id, floating_ip_id=ip_id) - - def _check_ips(self, server): - changed = False - - auto_ip = self.params['auto_ip'] - floating_ips = self.params['floating_ips'] - floating_ip_pools = self.params['floating_ip_pools'] - - if floating_ip_pools or floating_ips: - ips = openstack_find_nova_addresses(server.addresses, 'floating') - if not ips: - # If we're configured to have a floating but we don't have one, - # let's add one - server = self.conn.add_ips_to_server( - server, - auto_ip=auto_ip, - ips=floating_ips, - ip_pool=floating_ip_pools, - wait=self.params['wait'], - timeout=self.params['timeout'], - ) - changed = True - elif floating_ips: - # we were configured to have specific ips, let's make sure we have - # those - missing_ips = [] - for ip in floating_ips: - if ip not in ips: - missing_ips.append(ip) - if missing_ips: - server = self.conn.add_ip_list(server, missing_ips, - wait=self.params['wait'], - timeout=self.params['timeout']) - changed = True - extra_ips = [] - for ip in ips: - if ip not in floating_ips: - extra_ips.append(ip) - if extra_ips: - self._detach_ip_list(server, extra_ips) - changed = True - elif auto_ip: - if server['interface_ip']: - changed = False - else: - # We're configured for auto_ip but we're not showing an - # interface_ip. Maybe someone deleted an IP out from under us. - server = self.conn.add_ips_to_server( - server, - auto_ip=auto_ip, - ips=floating_ips, - ip_pool=floating_ip_pools, - wait=self.params['wait'], - timeout=self.params['timeout'], - ) - changed = True - return (changed, server) - - def _check_security_groups(self, server): - changed = False - - # server security groups were added to shade in 1.19. Until then this - # module simply ignored trying to update security groups and only set them - # on newly created hosts. - if not ( - hasattr(self.conn, 'add_server_security_groups') - and hasattr(self.conn, 'remove_server_security_groups') - ): - return changed, server - - module_security_groups = set(self.params['security_groups']) - server_security_groups = set(sg['name'] for sg in server.security_groups) - - add_sgs = module_security_groups - server_security_groups - remove_sgs = server_security_groups - module_security_groups - - if add_sgs: - self.conn.add_server_security_groups(server, list(add_sgs)) - changed = True - - if remove_sgs: - self.conn.remove_server_security_groups(server, list(remove_sgs)) - changed = True - - return (changed, server) + def _will_change(self, state, server): + if state == 'present' and not server: + return True + elif state == 'present' and server: + return bool(self._build_update(server)) + elif state == 'absent' and server: + return False + else: + # state == 'absent' and not server: + return True def main(): diff --git a/plugins/modules/server_info.py b/plugins/modules/server_info.py index 66aa7765..a3ef7920 100644 --- a/plugins/modules/server_info.py +++ b/plugins/modules/server_info.py @@ -14,10 +14,11 @@ description: notes: - The result contains a list of servers. options: - server: + name: description: - restrict results to servers with names or UUID matching - this glob expression (e.g., ). + this glob expression such as web*. + aliases: ['server'] type: str detailed: description: @@ -26,9 +27,10 @@ options: type: bool default: 'no' filters: - description: - - restrict results to servers matching a dictionary of - filters + description: | + Used for further filtering of results. Either a string containing a + JMESPath expression or a dictionary of meta data. Elements of the latter + may, themselves, be dictionaries. type: dict all_projects: description: @@ -45,15 +47,316 @@ extends_documentation_fragment: ''' EXAMPLES = ''' -# Gather information about all servers named that are in an active state: -- openstack.cloud.server_info: - cloud: rax-dfw - server: web* +- name: Gather information about all 'web*' servers in active state + openstack.cloud.server_info: + cloud: devstack + name: web* filters: vm_state: active - register: result -- debug: - msg: "{{ result.openstack_servers }}" + +- name: Filter servers with nested dictionaries + openstack.cloud.server_info: + cloud: devstack + filters: + metadata: + key1: value1 + key2: value2 +''' + +RETURN = ''' +servers: + description: List of servers matching the filters + elements: dict + type: list + returned: always + 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 @@ -62,7 +365,7 @@ from ansible_collections.openstack.cloud.plugins.module_utils.openstack import O class ServerInfoModule(OpenStackModule): argument_spec = dict( - server=dict(), + name=dict(aliases=['server']), detailed=dict(type='bool', default=False), filters=dict(type='dict'), all_projects=dict(type='bool', default=False), @@ -72,16 +375,14 @@ class ServerInfoModule(OpenStackModule): ) def run(self): + kwargs = dict((k, self.params[k]) + for k in ['detailed', 'filters', 'all_projects'] + if self.params[k] is not None) + kwargs['name_or_id'] = self.params['name'] - kwargs = self.check_versioned( - detailed=self.params['detailed'], - filters=self.params['filters'], - all_projects=self.params['all_projects'] - ) - if self.params['server']: - kwargs['name_or_id'] = self.params['server'] - openstack_servers = self.conn.search_servers(**kwargs) - self.exit(changed=False, openstack_servers=openstack_servers) + self.exit(changed=False, + servers=[server.to_dict(computed=False) for server in + self.conn.search_servers(**kwargs)]) def main(): diff --git a/plugins/modules/server_metadata.py b/plugins/modules/server_metadata.py index ae20e98e..11554474 100644 --- a/plugins/modules/server_metadata.py +++ b/plugins/modules/server_metadata.py @@ -31,11 +31,6 @@ options: choices: [present, absent] default: present type: str - availability_zone: - description: - - Availability zone in which to create the snapshot. - required: false - type: str requirements: - "python >= 3.6" - "openstacksdk" diff --git a/plugins/modules/volume.py b/plugins/modules/volume.py index 67c6f681..e3e07837 100644 --- a/plugins/modules/volume.py +++ b/plugins/modules/volume.py @@ -12,6 +12,10 @@ author: OpenStack Ansible SIG description: - Create or Remove cinder block storage volumes options: + availability_zone: + description: + - The availability zone. + type: str size: description: - Size of volume in GB. This parameter is required when the @@ -106,6 +110,7 @@ from ansible_collections.openstack.cloud.plugins.module_utils.openstack import O class VolumeModule(OpenStackModule): argument_spec = dict( + availability_zone=dict(type='str'), size=dict(type='int'), volume_type=dict(), display_name=dict(required=True, aliases=['name']),