Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions actions/server.snapshot.yaml
Original file line number Diff line number Diff line change
@@ -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
29 changes: 5 additions & 24 deletions lib/apis/openstack_api/openstack_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
):
Expand Down
87 changes: 21 additions & 66 deletions tests/lib/apis/openstack_api/test_openstack_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
delete_server,
snapshot_and_migrate_server,
snapshot_server,
wait_for_image_status,
wait_for_migration_status,
shutoff_server,
)
Expand Down Expand Up @@ -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
"""
Expand All @@ -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)
Expand All @@ -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",
Expand All @@ -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
Expand Down
Loading