diff --git a/actions/server.snapshot.yaml b/actions/server.snapshot.yaml new file mode 100644 index 000000000..448773015 --- /dev/null +++ b/actions/server.snapshot.yaml @@ -0,0 +1,27 @@ +--- +description: Snapshots an existing server into the admin project with the prefix `admin` and current time +enabled: true +entry_point: src/openstack_actions.py +name: server.snapshot +parameters: + lib_entry_point: + default: apis.openstack_api.openstack_server.snapshot_server + immutable: true + type: string + requires_openstack: + default: true + immutable: true + type: boolean + cloud_account: + description: "The clouds.yaml account to use whilst performing this action" + required: true + type: string + default: "dev" + enum: + - "dev" + - "prod" + server_id: + type: string + required: true + description: ID of server +runner_type: python-script diff --git a/lib/apis/openstack_api/openstack_server.py b/lib/apis/openstack_api/openstack_server.py index 208e1a74b..5593e2446 100644 --- a/lib/apis/openstack_api/openstack_server.py +++ b/lib/apis/openstack_api/openstack_server.py @@ -75,36 +75,17 @@ def snapshot_server(conn: Connection, server_id: str) -> Image: wait=True, timeout=21600, # 6 Hours ) - wait_for_image_status(conn, image, "active") + + assert isinstance(image.status, str) + if image.status.casefold() != "active".casefold(): # Wait flag implies image should be active from the start + raise ResourceFailure(f"Image {image.name} failed to upload.") + # Make VM's project image owner conn.image.update_image(image, owner=server.project_id) logger.info("Completed snapshot of server: %s", server.id) return image -def wait_for_image_status(conn: Connection, image, status, interval=5, timeout=3600): - """ - Waits for the status of the image to be the selected status - :param conn: Openstack connection - :param image: The Image object - :param status: The status of the image that is required - :param interval:How long to wait between checks - :param timeout: Timeout of the function - """ - if image.status == status: - return image - start_time = time.time() - while time.time() - start_time < timeout: - logger.info("Status of image %s: %s", image.id, image.status) - image = conn.image.get_image(image.id) - if image.status == status: - return image - if image.status == "error": - raise ResourceFailure(f"Image {image.name} failed to upload.") - time.sleep(interval) - raise ResourceTimeout(f"Timeout waiting for image {image.name} to become {status}.") - - def wait_for_migration_status( conn: Connection, server_id, status, interval=5, timeout=3600 ): diff --git a/tests/lib/apis/openstack_api/test_openstack_server.py b/tests/lib/apis/openstack_api/test_openstack_server.py index f7fd373de..3a303ace7 100644 --- a/tests/lib/apis/openstack_api/test_openstack_server.py +++ b/tests/lib/apis/openstack_api/test_openstack_server.py @@ -8,7 +8,6 @@ delete_server, snapshot_and_migrate_server, snapshot_server, - wait_for_image_status, wait_for_migration_status, shutoff_server, ) @@ -187,8 +186,7 @@ def test_migration_fail(mock_snapshot_server): mock_connection.compute.migrate_server.assert_not_called() -@patch("apis.openstack_api.openstack_server.wait_for_image_status") -def test_snapshot_server(mock_wait_for_image_status): +def test_snapshot_server(): """ Test server snapshot """ @@ -200,6 +198,7 @@ def test_snapshot_server(mock_wait_for_image_status): mock_connection.compute.find_server.return_value.project_id = mock_project_id mock_image = MagicMock() + mock_image.status = "aCTive" # Case-insensitivity check mock_connection.compute.create_server_image.return_value = mock_image snapshot_server(conn=mock_connection, server_id=mock_server_id) @@ -212,13 +211,29 @@ def test_snapshot_server(mock_wait_for_image_status): wait=True, timeout=21600, # 6 Hours ) - mock_wait_for_image_status.assert_called_once_with( - mock_connection, mock_image, "active" - ) + mock_connection.image.update_image.assert_called_once_with( mock_image, owner=mock_project_id ) +def test_snapshot_server_image_not_active(): + """ + Tests the snapshot server functionality checks the resulting image status is active + and will produce an error if not + """ + mock_connection = MagicMock() + mock_server_id = "server1" + mock_project_id = "project1" + mock_connection.compute.find_server.return_value.project_id = mock_project_id + + mock_image = MagicMock() + mock_connection.compute.create_server_image.return_value = mock_image + + for returned in ["error", "", " ", "pending"]: + mock_image.status = returned + with pytest.raises(ResourceFailure): + snapshot_server(conn=mock_connection, server_id=mock_server_id) + @pytest.mark.parametrize( "flavor_name,flavor_vcpu", @@ -245,66 +260,6 @@ def test_raise_invalid_migration(flavor_name, flavor_vcpu): ) -def test_wait_for_image_status_success(): - """ - Test wait_for_image_status when it is a success - """ - mock_conn = MagicMock() - image = MagicMock(id="123", name="test-image", status="pending") - image_final = MagicMock(id="123", name="test-image", status="active") - - mock_conn.image.get_image.side_effect = [ - MagicMock(id="123", name="test-image", status="pending"), - MagicMock(id="123", name="test-image", status="pending"), - image_final, - ] - result = wait_for_image_status(mock_conn, image, "active", interval=0, timeout=10) - mock_conn.image.get_image.assert_called_with(image.id) - assert result == image_final - - -def test_wait_for_image_status_timeout(): - """ - Test wait_for_image_status when it hits the timeout - """ - mock_conn = MagicMock() - image = MagicMock(id="123", name="test-image", status="pending") - mock_conn.image.get_image.side_effect = itertools.cycle([image]) - with pytest.raises( - ResourceTimeout, - match=f"Timeout waiting for image {image.name} to become active.", - ): - wait_for_image_status(mock_conn, image, "active", interval=0, timeout=0) - - -def test_wait_for_image_status_immediate_success(): - """ - Test wait_for_image_status when the image already has the desired status. - """ - mock_conn = MagicMock() - image = MagicMock(id="123", name="test-image", status="active") - result = wait_for_image_status(mock_conn, image, "active", interval=0, timeout=10) - assert result == image - mock_conn.image.get_image.assert_not_called() - - -def test_wait_for_image_status_error(): - """ - Test wait_for_image_status when the image status becomes error - """ - mock_conn = MagicMock() - image_pending = MagicMock(id="123", name="test-image", status="pending") - image_error = MagicMock(id="123", name="test-image", status="error") - mock_conn.image.get_image.side_effect = [ - image_pending, - image_error, - ] - with pytest.raises( - ResourceFailure, match=f"Image {image_error.name} failed to upload." - ): - wait_for_image_status(mock_conn, image_error, "active", interval=0, timeout=10) - - def test_wait_for_migration_status_success(): """ Test wait_for_migration_status when it is a success