Skip to content
Merged
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
106 changes: 91 additions & 15 deletions tools/generate_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"""

import argparse
import collections.abc
import sys
import time

Expand All @@ -27,6 +28,8 @@
UNFINISHED_IMAGE_STATUSES = frozenset({"", "queued", "saving"})
STOPPED = "SHUTOFF"
RUNNING = "ACTIVE"
# From here on the cloud failed to answer, rather than refused what was asked
SERVER_ERROR = 500
NODE_ABSENT = "{names} absent, a snapshot taken now could never be spawned back."


Expand Down Expand Up @@ -85,7 +88,7 @@ def _snapshot_instances(snapshots: dict[str, str], args: argparse.Namespace) ->

result = 0
try:
stopped.extend(instance_id for instance_id in snapshots if _stop(cloud, instance_id))
stopped.extend(instance_id for instance_id in snapshots if _stop(cloud, instance_id, args))
for instance_id in snapshots:
_wait_until_stopped(cloud, instance_id, args)

Expand Down Expand Up @@ -113,30 +116,54 @@ def _start_back(
ready to be used again rather than one still on its way up, and an
instance that never returns is worth hearing about here rather than at
whatever next tries to reach it.
"""
for instance_id in instance_ids:
print(f"Starting {instance_id} back")
cloud.compute.start_server(instance_id)

Every failure is reported rather than raised, one instance at a time.
This runs in the `finally` of whatever went before, where raising would
hide it, and would leave the instances after it down.
"""
back = True
started = []
for instance_id in instance_ids:
server = cloud.compute.get_server(instance_id)
try:
cloud.compute.wait_for_server(server, status=RUNNING, wait=args.timeout)
# NOTE: Settled first. This can run while a stop is still under
# way, and Nova refuses to start an instance it has not finished
# stopping.
server = _wait_until_settled(cloud, instance_id, args)
if server.status == RUNNING:
print(f"{instance_id} is already up")
continue

print(f"Starting {instance_id} back")
cloud.compute.start_server(instance_id)
started.append(instance_id)
except openstack.exceptions.SDKException as err:
print(f"{instance_id} could not be started back: {err}", file=sys.stderr)
back = False

for instance_id in started:
try:
server = _wait_until_settled(cloud, instance_id, args)
except openstack.exceptions.SDKException as err:
# NOTE: Reported rather than raised. This runs in the `finally`
# of whatever went before, where raising would hide it.
print(f"{instance_id} did not come back up: {err}", file=sys.stderr)
back = False
continue

if server.status != RUNNING:
print(f"{instance_id} did not come back up: {server.status}", file=sys.stderr)
back = False
else:
print(f"{instance_id} is back up")

return back


def _stop(cloud: openstack.connection.Connection, instance_id: str) -> bool:
def _stop(
cloud: openstack.connection.Connection,
instance_id: str,
args: argparse.Namespace,
) -> bool:
"""Ask an instance to stop, telling whether it had to be asked at all."""
server = cloud.compute.get_server(instance_id)
server = _ask(lambda: cloud.compute.get_server(instance_id), args)
if server.status == STOPPED:
return False

Expand All @@ -152,11 +179,60 @@ def _wait_until_stopped(
args: argparse.Namespace,
) -> None:
"""Hold until an instance is down, so that its disk stops moving."""
server = cloud.compute.get_server(instance_id)
cloud.compute.wait_for_server(server, status=STOPPED, wait=args.timeout)
server = _wait_until_settled(cloud, instance_id, args)
if server.status != STOPPED:
message = f"{instance_id} did not stop, it is {server.status}"
raise openstack.exceptions.ResourceFailure(message)

print(f"{instance_id} stopped")


def _wait_until_settled(
cloud: openstack.connection.Connection,
instance_id: str,
args: argparse.Namespace,
) -> openstack.compute.v2.server.Server:
"""
Hold until Nova is done with whatever it was doing to an instance.

NOTE: Settled rather than in a given status. Nova marks an instance busy
before it answers a stop or a start, and clears the mark once it is done,
whether it went through or not. Waiting on a status instead holds until
the timeout when the action fails, and tells nothing of why.
"""
deadline = time.monotonic() + args.timeout
while True:
server = _ask(lambda: cloud.compute.get_server(instance_id), args)
if server.task_state is None:
return server
if time.monotonic() >= deadline:
message = f"{instance_id} is still {server.task_state} after {args.timeout}s"
raise openstack.exceptions.ResourceTimeout(message)

time.sleep(args.poll)


def _ask[T](call: collections.abc.Callable[[], T], args: argparse.Namespace) -> T:
"""
Read something from the cloud, asking again while it fails to answer.

NOTE: Only for reads, which can be asked again without consequence. A
server error says nothing of the instance itself: Nova answers one at
times when one of the services behind it does not answer it in time.
"""
deadline = time.monotonic() + args.timeout
while True:
try:
return call()
except openstack.exceptions.HttpException as err:
answered = err.status_code is None or err.status_code < SERVER_ERROR
if answered or time.monotonic() >= deadline:
raise
print(f" the cloud failed to answer ({err.status_code}), asking again")

time.sleep(args.poll)


def _take_snapshot(
cloud: openstack.connection.Connection,
instance_id: str,
Expand Down Expand Up @@ -205,9 +281,9 @@ def _wait_until_active(
"""
deadline = time.monotonic() + args.timeout
while time.monotonic() < deadline:
image = cloud.image.find_image(image_id)
image = _ask(lambda: cloud.image.find_image(image_id), args)
status = "" if image is None else image.status
task = cloud.compute.get_server(instance_id).task_state
task = _ask(lambda: cloud.compute.get_server(instance_id), args).task_state

if status not in UNFINISHED_IMAGE_STATUSES and task != UPLOADING_TASK:
return status == "active"
Expand Down
Loading