diff --git a/ci/roles/server/tasks/main.yml b/ci/roles/server/tasks/main.yml index 4ac6342e..5e9ef9e0 100644 --- a/ci/roles/server/tasks/main.yml +++ b/ci/roles/server/tasks/main.yml @@ -607,5 +607,3 @@ 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_action/tasks/main.yml similarity index 68% rename from ci/roles/server/tasks/server_actions.yml rename to ci/roles/server_action/tasks/main.yml index d710ee80..732645c3 100644 --- a/ci/roles/server/tasks/server_actions.yml +++ b/ci/roles/server_action/tasks/main.yml @@ -1,8 +1,19 @@ --- +- name: List all images + openstack.cloud.image_info: + cloud: "{{ cloud }}" + register: images + +- name: Identify CirrOS image name + set_fact: + image_name: "{{ images.images|community.general.json_query(query)|first }}" + vars: + query: "[?starts_with(name, 'cirros')].name" + - name: Create network for server openstack.cloud.network: cloud: "{{ cloud }}" - name: "{{ server_network }}" + name: ansible_server_network state: present register: network @@ -10,8 +21,8 @@ openstack.cloud.subnet: cidr: 192.168.0.0/24 cloud: "{{ cloud }}" - name: "{{ server_subnet }}" - network_name: "{{ server_network }}" + name: ansible_server_subnet + network_name: ansible_server_network state: present register: subnet @@ -19,10 +30,10 @@ openstack.cloud.server: cloud: "{{ cloud }}" state: present - name: "{{ server_name }}" + name: ansible_server image: "{{ image_name }}" - flavor: "{{ flavor_name }}" - network: "{{ server_network }}" + flavor: m1.tiny + network: ansible_server_network auto_floating_ip: false wait: true register: server @@ -30,18 +41,18 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info1 + server: ansible_server + register: servers - name: Ensure status for server is ACTIVE assert: that: - - info1.servers.0.status == 'ACTIVE' + - servers.servers.0.status == 'ACTIVE' - name: Stop server openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server action: stop wait: true register: server @@ -49,19 +60,19 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info2 + server: ansible_server + register: servers - name: Ensure status for server is SHUTOFF assert: that: - - info2.servers.0.status == 'SHUTOFF' + - servers.servers.0.status == 'SHUTOFF' - server is changed - name: Stop server again openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server action: stop wait: true register: server @@ -69,19 +80,19 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info3 + server: ansible_server + register: servers - name: Ensure status for server is SHUTOFF assert: that: - - info3.servers.0.status == 'SHUTOFF' + - servers.servers.0.status == 'SHUTOFF' - server is not changed - name: Start server openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server action: start wait: true register: server @@ -89,19 +100,19 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info4 + server: ansible_server + register: servers - name: Ensure status for server is ACTIVE assert: that: - - info4.servers.0.status == 'ACTIVE' + - servers.servers.0.status == 'ACTIVE' - server is changed - name: Start server again openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server action: start wait: true register: server @@ -109,19 +120,19 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info5 + server: ansible_server + register: servers - name: Ensure status for server is ACTIVE assert: that: - - info5.servers.0.status == 'ACTIVE' + - servers.servers.0.status == 'ACTIVE' - server is not changed - name: Pause server openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server action: pause wait: true register: server @@ -129,19 +140,19 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info6 + server: ansible_server + register: servers - name: Ensure status for server is PAUSED assert: that: - - info6.servers.0.status == 'PAUSED' + - servers.servers.0.status == 'PAUSED' - server is changed - name: Pause server again openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server action: pause wait: true register: server @@ -149,19 +160,19 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info7 + server: ansible_server + register: servers - name: Ensure status for server is PAUSED assert: that: - - info7.servers.0.status == 'PAUSED' + - servers.servers.0.status == 'PAUSED' - server is not changed - name: Unpause server openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server action: unpause wait: true register: server @@ -169,19 +180,19 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info8 + server: ansible_server + register: servers - name: Ensure status for server is ACTIVE assert: that: - - info8.servers.0.status == 'ACTIVE' + - servers.servers.0.status == 'ACTIVE' - server is changed - name: Unpause server again openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server action: unpause wait: true register: server @@ -189,19 +200,19 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info9 + server: ansible_server + register: servers - name: Ensure status for server is ACTIVE assert: that: - - info9.servers.0.status == 'ACTIVE' + - servers.servers.0.status == 'ACTIVE' - server is not changed - name: Lock server openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server action: lock wait: true register: server @@ -209,20 +220,20 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info10 + server: ansible_server + register: servers - name: Ensure status for server is ACTIVE assert: that: - - info10.servers.0.status == 'ACTIVE' - - info10.servers.0.is_locked + - servers.servers.0.status == 'ACTIVE' + - servers.servers.0.is_locked - server is changed - name: Lock server again openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server action: lock wait: true register: server @@ -230,20 +241,20 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info11 + server: ansible_server + register: servers - name: Ensure status for server is ACTIVE assert: that: - - info11.servers.0.status == 'ACTIVE' - - info11.servers.0.is_locked - - server is changed # no support for lock idempotency + - servers.servers.0.status == 'ACTIVE' + - servers.servers.0.is_locked + - server is not changed - name: Unock server openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server action: unlock wait: true register: server @@ -251,20 +262,20 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info12 + server: ansible_server + register: servers - name: Ensure status for server is ACTIVE assert: that: - - info12.servers.0.status == 'ACTIVE' - - not info12.servers.0.is_locked + - servers.servers.0.status == 'ACTIVE' + - not servers.servers.0.is_locked - server is changed - name: Unlock server again openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server action: unlock wait: true register: server @@ -272,20 +283,20 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info13 + server: ansible_server + register: servers - name: Ensure status for server is ACTIVE assert: that: - - info13.servers.0.status == 'ACTIVE' - - server is changed # no support for unlock idempotency - - not info13.servers.0.is_locked + - servers.servers.0.status == 'ACTIVE' + - server is not changed + - not servers.servers.0.is_locked - name: Suspend server openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server action: suspend wait: true register: server @@ -293,19 +304,19 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info14 + server: ansible_server + register: servers - name: Ensure status for server is SUSPENDED assert: that: - - info14.servers.0.status == 'SUSPENDED' + - servers.servers.0.status == 'SUSPENDED' - server is changed - name: Suspend server again openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server action: suspend wait: true register: server @@ -313,19 +324,19 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info15 + server: ansible_server + register: servers - name: Ensure status for server is SUSPENDED assert: that: - - info15.servers.0.status == 'SUSPENDED' + - servers.servers.0.status == 'SUSPENDED' - server is not changed - name: Resume server openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server action: resume wait: true register: server @@ -333,19 +344,19 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info16 + server: ansible_server + register: servers - name: Ensure status for server is ACTIVE assert: that: - - info16.servers.0.status == 'ACTIVE' + - servers.servers.0.status == 'ACTIVE' - server is changed - name: Resume server again openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server action: resume wait: true register: server @@ -353,19 +364,19 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info17 + server: ansible_server + register: servers - name: Ensure status for server is ACTIVE assert: that: - - info17.servers.0.status == 'ACTIVE' + - servers.servers.0.status == 'ACTIVE' - server is not changed - name: Rebuild server - error openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server action: rebuild wait: true register: server @@ -380,7 +391,7 @@ - name: Rebuild server openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server image: "{{ image_name }}" action: rebuild wait: true @@ -389,19 +400,19 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info18 + server: ansible_server + register: servers - name: Ensure status for server is ACTIVE assert: that: - - info18.servers.0.status in ('ACTIVE', 'REBUILD') + - servers.servers.0.status in ('ACTIVE', 'REBUILD') - server is changed - name: Rebuild server with admin password openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server image: "{{ image_name }}" action: rebuild wait: true @@ -411,19 +422,19 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info19 + server: ansible_server + register: servers - name: Ensure status for server is ACTIVE assert: that: - - info19.servers.0.status in ('ACTIVE', 'REBUILD') + - servers.servers.0.status in ('ACTIVE', 'REBUILD') - server is changed - name: Shelve server openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server action: shelve wait: true register: server @@ -431,39 +442,48 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info20 + server: ansible_server + register: servers - name: Ensure status for server is SHELVED or SHELVED_OFFLOADED assert: that: - - info20.servers.0.status in ['SHELVED', 'SHELVED_OFFLOADED'] + - servers.servers.0.status in ['SHELVED', 'SHELVED_OFFLOADED'] - server is changed - name: Shelve offload server openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server action: shelve_offload wait: true register: server + ignore_errors: true + +- name: Assert shelve offload server + assert: + that: + - ((server is success) + or (server is not success + and "Cannot 'shelveOffload' instance" in server.msg + and "while it is in vm_state shelved_offloaded" in server.msg)) - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info21 + server: ansible_server + register: servers - name: Ensure status for server is SHELVED_OFFLOADED # no change if server has been offloaded automatically after first shelve command assert: that: - - info21.servers.0.status == 'SHELVED_OFFLOADED' + - servers.servers.0.status == 'SHELVED_OFFLOADED' - name: Shelve offload server again openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server action: shelve_offload wait: true register: server @@ -471,19 +491,19 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info22 + server: ansible_server + register: servers - name: Ensure status for server is SHELVED_OFFLOADED assert: that: - - info22.servers.0.status == 'SHELVED_OFFLOADED' + - servers.servers.0.status == 'SHELVED_OFFLOADED' - server is not changed - name: Unshelve server openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server action: unshelve wait: true register: server @@ -491,19 +511,19 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info23 + server: ansible_server + register: servers - name: Ensure status for server is ACTIVE assert: that: - - info23.servers.0.status == 'ACTIVE' + - servers.servers.0.status == 'ACTIVE' - server is changed - name: Unshelve server again openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server action: unshelve wait: true register: server @@ -511,19 +531,19 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info24 + server: ansible_server + register: servers - name: Ensure status for server is ACTIVE assert: that: - - info24.servers.0.status == 'ACTIVE' + - servers.servers.0.status == 'ACTIVE' - server is not changed - name: Reboot server (SOFT) openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server action: reboot_soft wait: true register: server @@ -531,19 +551,19 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info25 + server: ansible_server + register: servers - name: Ensure status for server is ACTIVE assert: that: - - info25.servers.0.status == 'ACTIVE' + - servers.servers.0.status == 'ACTIVE' - server is not changed - name: Reboot server (HARD) openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_name }}" + server: ansible_server action: reboot_hard wait: true register: server @@ -551,33 +571,33 @@ - name: Get info about server openstack.cloud.server_info: cloud: "{{ cloud }}" - server: "{{ server_name }}" - register: info26 + server: ansible_server + register: servers - name: Ensure status for server is ACTIVE assert: that: - - info26.servers.0.status == 'ACTIVE' + - servers.servers.0.status == 'ACTIVE' - server is not changed - name: Delete server openstack.cloud.server: cloud: "{{ cloud }}" - name: "{{ server_name }}" + name: ansible_server state: absent wait: true - name: Create network for alternate server openstack.cloud.network: cloud: "{{ cloud_alt }}" - name: "{{ server_alt_network }}" + name: ansible_server_network2 state: present - name: Create subnet for alternate server openstack.cloud.subnet: cloud: "{{ cloud_alt }}" - network_name: "{{ server_alt_network }}" - name: "{{ server_alt_subnet }}" + network_name: ansible_server_network2 + name: ansible_server_subnet2 state: present cidr: 192.168.0.0/24 @@ -585,88 +605,88 @@ openstack.cloud.server: cloud: "{{ cloud_alt }}" state: present - name: "{{ server_alt_name }}" + name: ansible_server2 image: "{{ image_name }}" - flavor: "{{ flavor_name }}" - network: "{{ server_alt_network }}" + flavor: m1.tiny + network: ansible_server_network2 auto_floating_ip: false wait: true - register: server_alt + register: server - name: Get info about server in alternate project openstack.cloud.server_info: cloud: "{{ cloud_alt }}" - server: "{{ server_alt_name }}" - register: info27 + server: ansible_server2 + register: servers - name: Ensure status for server in alternate project is ACTIVE assert: that: - - info27.servers.0.status == 'ACTIVE' + - servers.servers.0.status == 'ACTIVE' - name: Try to stop server in alternate project openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_alt_name }}" + server: ansible_server2 action: stop wait: true ignore_errors: true - register: server_alt + register: server - name: Ensure server was not stopped assert: that: - - server_alt is failed - - server_alt.msg == "Could not find server {{ server_alt_name }}" + - server is failed + - server.msg == "No Server found for ansible_server2" - name: Stop server in alternate project with all_projects=true openstack.cloud.server_action: cloud: "{{ cloud }}" - server: "{{ server_alt_name }}" + server: ansible_server2 action: stop wait: true all_projects: True - register: server_alt + register: server - name: Get info about server in alternate project openstack.cloud.server_info: cloud: "{{ cloud_alt }}" - server: "{{ server_alt_name }}" - register: info26 + server: ansible_server2 + register: servers - name: Ensure status for server is SHUTOFF assert: that: - - info26.servers.0.status == 'SHUTOFF' - - server_alt is changed + - servers.servers.0.status == 'SHUTOFF' + - server is changed - name: Delete server in alternate project openstack.cloud.server: cloud: "{{ cloud_alt }}" state: absent - name: "{{ server_alt_name }}" + name: ansible_server2 wait: true - name: Delete subnet for alternate server openstack.cloud.subnet: cloud: "{{ cloud_alt }}" - name: "{{ server_alt_subnet }}" + name: ansible_server_subnet2 state: absent - name: Delete network for alternate server openstack.cloud.network: cloud: "{{ cloud_alt }}" - name: "{{ server_alt_network }}" + name: ansible_server_network2 state: absent - name: Delete subnet for server openstack.cloud.subnet: cloud: "{{ cloud }}" state: absent - name: "{{ server_subnet }}" + name: ansible_server_subnet - name: Delete network for server openstack.cloud.network: cloud: "{{ cloud }}" state: absent - name: "{{ server_network }}" + name: ansible_server_network diff --git a/ci/run-collection.yml b/ci/run-collection.yml index a5d71c15..80e830e5 100644 --- a/ci/run-collection.yml +++ b/ci/run-collection.yml @@ -47,6 +47,7 @@ - { role: security_group, tags: security_group } - { role: security_group_rule, tags: security_group_rule } - { role: server, tags: server } + - { role: server_action, tags: server_action } - { role: server_group, tags: server_group } - { role: server_metadata, tags: server_metadata } - { role: server_volume, tags: server_volume } diff --git a/plugins/modules/server_action.py b/plugins/modules/server_action.py index 8b5d74c0..a68055b9 100644 --- a/plugins/modules/server_action.py +++ b/plugins/modules/server_action.py @@ -4,236 +4,208 @@ # Copyright (c) 2015, Jesse Keating # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -DOCUMENTATION = ''' +DOCUMENTATION = r''' --- module: server_action -short_description: Perform actions on Compute Instances from OpenStack +short_description: Perform actions on OpenStack compute (Nova) instances author: OpenStack Ansible SIG description: - - Perform server actions on an existing compute instance from OpenStack. - This module does not return any data other than changed true/false. - When I(action) is 'rebuild', then I(image) parameter is required. + - Perform actions on OpenStack compute (Nova) instances aka servers. options: - server: - description: - - Name or ID of the instance - required: true - type: str - wait: - description: - - If the module should wait for the instance action to be performed. - type: bool - default: 'yes' - timeout: - description: - - The amount of time the module should wait for the instance to perform - the requested action. - default: 180 - type: int - action: - description: - - Perform the given action. The lock and unlock actions always return - changed as the servers API does not provide lock status. - choices: [stop, start, pause, unpause, lock, unlock, suspend, - reboot_soft, reboot_hard, resume, rebuild, shelve, - shelve_offload, unshelve] - type: str - required: true - image: - description: - - Image the server should be rebuilt with - type: str - admin_password: - description: - - Admin password for server to rebuild - type: str - all_projects: - description: - - Whether to search for server in all projects or just the current - auth scoped project. - type: bool - default: 'no' + action: + description: + - Action to perform. + - By default, only server owners and administrators are allowed to + perform actions C(pause), C(unpause), C(suspend), C(resume), C(lock), + C(unlock) and C(shelve_offload). + choices: [lock, pause, reboot_hard, reboot_soft, rebuild, resume, shelve, + shelve_offload, start, stop, suspend, unlock, unpause, unshelve] + type: str + required: true + admin_password: + description: + - Admin password for server to rebuild. + type: str + all_projects: + description: + - Whether to search for server in all projects or the current project + only. + type: bool + default: false + image: + description: + - Image name or ID the server should be rebuilt with. + type: str + name: + description: + - Server name or ID. + required: true + type: str + aliases: ['server'] extends_documentation_fragment: -- openstack.cloud.openstack + - openstack.cloud.openstack ''' -EXAMPLES = ''' -# Pauses a compute instance -- openstack.cloud.server_action: - action: pause - auth: - auth_url: https://identity.example.com - username: admin - password: admin - project_name: admin - server: vm1 - timeout: 200 +EXAMPLES = r''' +- name: Pauses a compute instance + openstack.cloud.server_action: + cloud: devstack-admin + action: pause + server: vm1 + timeout: 200 ''' from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule -# If I(action) is set to C(shelve) then according to OpenStack's Compute API, the shelved -# server is in one of two possible states: -# -# SHELVED: The server is in shelved state. Depends on the shelve offload time, -# the server will be automatically shelved off loaded. -# SHELVED_OFFLOADED: The shelved server is offloaded (removed from the compute host) and -# it needs unshelved action to be used again. -# -# But wait_for_server can only wait for a single server state. If a shelved server is offloaded -# immediately, then a exceptions.ResourceTimeout will be raised if I(action) is set to C(shelve). -# This is likely to happen because shelved_offload_time in Nova's config is set to 0 by default. -# This also applies if you boot the server from volumes. -# -# Calling C(shelve_offload) instead of C(shelve) will also fail most likely because the default -# policy does not allow C(shelve_offload) for non-admin users while C(shelve) is allowed for -# admin users and server owners. -# -# As we cannot retrieve shelved_offload_time from Nova's config, we fall back to waiting for -# one state and if that fails then we fetch the server's state and match it against the other -# valid states from _action_map. -# -# Ref.: https://docs.openstack.org/api-guide/compute/server_concepts.html - -_action_map = {'stop': ['SHUTOFF'], - 'start': ['ACTIVE'], - 'pause': ['PAUSED'], - 'unpause': ['ACTIVE'], - 'lock': ['ACTIVE'], # API doesn't show lock/unlock status - 'unlock': ['ACTIVE'], - 'suspend': ['SUSPENDED'], - 'reboot_soft': ['ACTIVE'], - 'reboot_hard': ['ACTIVE'], - 'resume': ['ACTIVE'], - 'rebuild': ['ACTIVE'], - 'shelve': ['SHELVED_OFFLOADED', 'SHELVED'], - 'shelve_offload': ['SHELVED_OFFLOADED'], - 'unshelve': ['ACTIVE']} - -_admin_actions = ['pause', 'unpause', 'suspend', 'resume', 'lock', 'unlock', 'shelve_offload'] - class ServerActionModule(OpenStackModule): argument_spec = dict( - server=dict(required=True), action=dict(required=True, choices=['stop', 'start', 'pause', 'unpause', - 'lock', 'unlock', 'suspend', 'reboot_soft', 'reboot_hard', 'resume', - 'rebuild', 'shelve', 'shelve_offload', 'unshelve']), - image=dict(), + 'lock', 'unlock', 'suspend', 'reboot_soft', + 'reboot_hard', 'resume', 'rebuild', 'shelve', + 'shelve_offload', 'unshelve']), admin_password=dict(no_log=True), all_projects=dict(type='bool', default=False), + image=dict(), + name=dict(required=True, aliases=['server']), ) + module_kwargs = dict( required_if=[('action', 'rebuild', ['image'])], supports_check_mode=True, ) + # If I(action) is set to C(shelve) then according to OpenStack's Compute + # API, the shelved server is in one of two possible states: + # + # SHELVED: The server is in shelved state. Depends on the shelve + # offload time, the server will be automatically + # shelved offloaded. + # SHELVED_OFFLOADED: The shelved server is offloaded (removed from the + # compute host) and it needs unshelved action to be + # used again. + # + # But wait_for_server can only wait for a single server state. If a shelved + # server is offloaded immediately, then a exceptions.ResourceTimeout will + # be raised if I(action) is set to C(shelve). This is likely to happen + # because shelved_offload_time in Nova's config is set to 0 by default. + # This also applies if you boot the server from volumes. + # + # Calling C(shelve_offload) instead of C(shelve) will also fail most likely + # because the default policy does not allow C(shelve_offload) for non-admin + # users while C(shelve) is allowed for admin users and server owners. + # + # As we cannot retrieve shelved_offload_time from Nova's config, we fall + # back to waiting for one state and if that fails then we fetch the + # server's state and match it against the other valid states from + # _action_map. + # + # Ref.: https://docs.openstack.org/api-guide/compute/server_concepts.html + + _action_map = {'stop': ['SHUTOFF'], + 'start': ['ACTIVE'], + 'pause': ['PAUSED'], + 'unpause': ['ACTIVE'], + 'lock': ['ACTIVE'], + 'unlock': ['ACTIVE'], + 'suspend': ['SUSPENDED'], + 'reboot_soft': ['ACTIVE'], + 'reboot_hard': ['ACTIVE'], + 'resume': ['ACTIVE'], + 'rebuild': ['ACTIVE'], + 'shelve': ['SHELVED_OFFLOADED', 'SHELVED'], + 'shelve_offload': ['SHELVED_OFFLOADED'], + 'unshelve': ['ACTIVE']} + def run(self): - os_server = self._preliminary_checks() - self._execute_server_action(os_server) - # for some reason we don't wait for lock and unlock before exit - if self.params['action'] not in ('lock', 'unlock'): - if self.params['wait']: - self._wait(os_server) - self.exit_json(changed=True) + # TODO: Replace with self.conn.compute.find_server( + # self.params['name'], all_projects=self.params['all_projects'], + # ignore_missing=False) when [0] has been merged. + # [0] https://review.opendev.org/c/openstack/openstacksdk/+/857936/ + server = self.conn.get_server( + name_or_id=self.params['name'], + detailed=True, + all_projects=self.params['all_projects']) + if not server: + self.fail_json(msg='No Server found for {0}' + .format(self.params['name'])) - def _preliminary_checks(self): - # Using Munch object for getting information about a server - os_server = self.conn.get_server( - self.params['server'], - all_projects=self.params['all_projects'], - ) - if not os_server: - self.fail_json(msg='Could not find server %s' % self.params['server']) - # check mode - if self.ansible.check_mode: - self.exit_json(changed=self.__system_state_change(os_server)) - # examine special cases - # lock, unlock and rebuild don't depend on state, just do it - if self.params['action'] not in ('lock', 'unlock', 'rebuild'): - if not self.__system_state_change(os_server): - self.exit_json(changed=False) - return os_server + action = self.params['action'] - def _execute_server_action(self, os_server): - if self.params['action'] == 'rebuild': - return self._rebuild_server(os_server) - if self.params['action'] == 'shelve_offload': - # shelve_offload is not supported in OpenstackSDK - return self._action(os_server, json={'shelveOffload': None}) - action_name = self.params['action'] + "_server" + # rebuild does not depend on state + will_change = ( + (action == 'rebuild') + or (action == 'lock' and not server['is_locked']) + or (action == 'unlock' and server['is_locked']) + or server.status.lower() not in [a.lower() + for a + in self._action_map[action]]) - # reboot_* actions are using reboot_server method with an extra param - if self.params['action'] == 'reboot_soft' or self.params['action'] == 'reboot_hard': - action_name = 'reboot_server' + if not will_change: + self.exit_json(changed=False) + elif self.ansible.check_mode: + self.exit_json(changed=True) + # else perform action + + if action == 'rebuild': + # rebuild should ensure images exists + image = self.conn.image.find_image(self.params['image'], + ignore_missing=False) + kwargs = dict(server=server, + name=server['name'], + image=image['id']) + + admin_password = self.params['admin_password'] + if admin_password is not None: + kwargs['admin_password'] = admin_password + + self.conn.compute.rebuild_server(**kwargs) + elif action == 'shelve_offload': + # TODO: Replace with shelve_offload function call when [0] has been + # merged. + # [0] https://review.opendev.org/c/openstack/openstacksdk/+/857947 + + # shelve_offload is not supported in openstacksdk <= 0.103.0 + response = self.conn.compute.post( + '/servers/{server_id}/action'.format(server_id=server['id']), + json={'shelveOffload': None}) + self.sdk.exceptions.raise_from_response(response) + else: # action != 'rebuild' and action != 'shelve_offload' + action_name = action + "_server" + + # reboot_* actions are using reboot_server method with an + # additional argument + if action in ['reboot_soft', 'reboot_hard']: + action_name = 'reboot_server' - # Try to grab method from OpenstackSDK - try: func_name = getattr(self.conn.compute, action_name) - except AttributeError: - self.fail_json( - msg="Method %s wasn't found in OpenstackSDK compute" % action_name) - # Do the action - if self.params['action'] == 'reboot_soft': - func_name(os_server, 'SOFT') - elif self.params['action'] == 'reboot_hard': - func_name(os_server, 'HARD') - else: - func_name(os_server) + # Do the action + if action == 'reboot_soft': + func_name(server, 'SOFT') + elif action == 'reboot_hard': + func_name(server, 'HARD') + else: + func_name(server) - def _rebuild_server(self, os_server): - # rebuild should ensure images exists - try: - image = self.conn.get_image(self.params['image']) - except Exception as e: - self.fail_json( - msg="Can't find the image %s: %s" % (self.params['image'], e)) - if not image: - self.fail_json(msg="Image %s was not found!" % self.params['image']) - # admin_password is required by SDK, but not required by Nova API - if self.params['admin_password']: - self.conn.compute.rebuild_server( - server=os_server, - name=os_server['name'], - image=image['id'], - admin_password=self.params['admin_password'] - ) - else: - self._action(os_server, json={'rebuild': {'imageRef': image['id']}}) + if self.params['wait']: + for count in self.sdk.utils.iterate_timeout( + timeout=self.params['timeout'], + message='Timeout waiting for action {0} to be completed.' + .format(action) + ): + server = self.conn.compute.get_server(server['id']) - def _action(self, os_server, json): - response = self.conn.compute.post( - '/servers/{server_id}/action'.format(server_id=os_server['id']), - json=json) - self.sdk.exceptions.raise_from_response(response) - return response + if (action == 'lock' and server['is_locked']) \ + or (action == 'unlock' and not server['is_locked']): + break - def _wait(self, os_server): - """Wait for the server to reach the desired state for the given action.""" - # The wait_for_server function needs a Server object instead of the - # Munch object returned by self.conn.get_server - server = self.conn.compute.get_server(os_server['id']) - states = _action_map[self.params['action']] + states = [s.lower() for s in self._action_map[action]] + if server.status.lower() in states: + break - try: - self.conn.compute.wait_for_server( - server, - status=states[0], - wait=self.params['timeout']) - except self.sdk.exceptions.ResourceTimeout: - # raise if there is only one valid state - if len(states) < 2: - raise - # fetch current server status and compare to other valid states - server = self.conn.compute.get_server(os_server['id']) - if server.status not in states: - raise - - def __system_state_change(self, os_server): - """Check if system state would change.""" - return os_server.status not in _action_map[self.params['action']] + self.exit_json(changed=True) def main():