diff --git a/ci/roles/server/tasks/server_actions.yml b/ci/roles/server/tasks/server_actions.yml index 8f5b9bc7..ede97a9c 100644 --- a/ci/roles/server/tasks/server_actions.yml +++ b/ci/roles/server/tasks/server_actions.yml @@ -418,3 +418,103 @@ that: - info19.openstack_servers.0.status in ('ACTIVE', 'REBUILD') - server is changed + +- name: Shelve server + openstack.cloud.server_action: + cloud: "{{ cloud }}" + server: "{{ server_name }}" + action: shelve + wait: true + register: server + +- name: Get info about server + openstack.cloud.server_info: + cloud: "{{ cloud }}" + server: "{{ server_name }}" + register: info20 + +- name: Ensure status for server is SHELVED or SHELVED_OFFLOADED + assert: + that: + - info20.openstack_servers.0.status in ['SHELVED', 'SHELVED_OFFLOADED'] + - server is changed + +- name: Shelve offload server + openstack.cloud.server_action: + cloud: "{{ cloud }}" + server: "{{ server_name }}" + action: shelve_offload + wait: true + register: server + +- name: Get info about server + openstack.cloud.server_info: + cloud: "{{ cloud }}" + server: "{{ server_name }}" + register: info21 + +- name: Ensure status for server is SHELVED_OFFLOADED + # no change if server has been offloaded automatically after first shelve command + assert: + that: + - info21.openstack_servers.0.status == 'SHELVED_OFFLOADED' + +- name: Shelve offload server again + openstack.cloud.server_action: + cloud: "{{ cloud }}" + server: "{{ server_name }}" + action: shelve_offload + wait: true + register: server + +- name: Get info about server + openstack.cloud.server_info: + cloud: "{{ cloud }}" + server: "{{ server_name }}" + register: info22 + +- name: Ensure status for server is SHELVED_OFFLOADED + assert: + that: + - info22.openstack_servers.0.status == 'SHELVED_OFFLOADED' + - server is not changed + +- name: Unshelve server + openstack.cloud.server_action: + cloud: "{{ cloud }}" + server: "{{ server_name }}" + action: unshelve + wait: true + register: server + +- name: Get info about server + openstack.cloud.server_info: + cloud: "{{ cloud }}" + server: "{{ server_name }}" + register: info23 + +- name: Ensure status for server is ACTIVE + assert: + that: + - info23.openstack_servers.0.status == 'ACTIVE' + - server is changed + +- name: Unshelve server again + openstack.cloud.server_action: + cloud: "{{ cloud }}" + server: "{{ server_name }}" + action: unshelve + wait: true + register: server + +- name: Get info about server + openstack.cloud.server_info: + cloud: "{{ cloud }}" + server: "{{ server_name }}" + register: info24 + +- name: Ensure status for server is ACTIVE + assert: + that: + - info24.openstack_servers.0.status == 'ACTIVE' + - server is not changed diff --git a/plugins/modules/server_action.py b/plugins/modules/server_action.py index 68bd62b6..2a34782d 100644 --- a/plugins/modules/server_action.py +++ b/plugins/modules/server_action.py @@ -35,7 +35,7 @@ options: - 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, resume, - rebuild] + rebuild, shelve, shelve_offload, unshelve] type: str required: true image: @@ -70,18 +70,43 @@ EXAMPLES = ''' 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', - 'resume': 'ACTIVE', - 'rebuild': 'ACTIVE'} +_action_map = {'stop': ['SHUTOFF'], + 'start': ['ACTIVE'], + 'pause': ['PAUSED'], + 'unpause': ['ACTIVE'], + 'lock': ['ACTIVE'], # API doesn't show lock/unlock status + 'unlock': ['ACTIVE'], + 'suspend': ['SUSPENDED'], + 'resume': ['ACTIVE'], + 'rebuild': ['ACTIVE'], + 'shelve': ['SHELVED_OFFLOADED', 'SHELVED'], + 'shelve_offload': ['SHELVED_OFFLOADED'], + 'unshelve': ['ACTIVE']} -_admin_actions = ['pause', 'unpause', 'suspend', 'resume', 'lock', 'unlock'] +_admin_actions = ['pause', 'unpause', 'suspend', 'resume', 'lock', 'unlock', 'shelve_offload'] class ServerActionModule(OpenStackModule): @@ -92,7 +117,7 @@ class ServerActionModule(OpenStackModule): action=dict(required=True, type='str', choices=['stop', 'start', 'pause', 'unpause', 'lock', 'unlock', 'suspend', 'resume', - 'rebuild']), + 'rebuild', 'shelve', 'shelve_offload', 'unshelve']), image=dict(required=False, type='str'), admin_password=dict(required=False, type='str'), ) @@ -128,6 +153,9 @@ class ServerActionModule(OpenStackModule): 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" try: func_name = getattr(self.conn.compute, action_name) @@ -154,23 +182,38 @@ class ServerActionModule(OpenStackModule): admin_password=self.params['admin_password'] ) else: - self.conn.compute.post( - '/servers/{server_id}/action'.format( - server_id=os_server['id']), - json={'rebuild': {'imageRef': image['id']}}) + self._action(os_server, json={'rebuild': {'imageRef': image['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 def _wait(self, os_server): """Wait for the server to reach the desired state for the given action.""" # Using Server object for wait_for_server function server = self.conn.compute.find_server(self.params['server']) - self.conn.compute.wait_for_server( - server, - status=_action_map[self.params['action']], - wait=self.params['timeout']) + states = _action_map[self.params['action']] + + 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 != _action_map[self.params['action']] + return os_server.status not in _action_map[self.params['action']] def main():