Skip to content
Merged
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
78 changes: 78 additions & 0 deletions osism/commands/get.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from datetime import datetime
import pprint
import re
import subprocess
import json

Expand Down Expand Up @@ -263,6 +264,83 @@ def take_action(self, parsed_args):
return


# Printed once by kolla-mariadb-backup-host.yml: the host the backup runs on
# (empty if none) and the host each shard resolved.
BACKUP_HOST_PATTERN = re.compile(r"mariadb_backup_host=([^\s\"]*) resolved=([^\s\"]*)")


class MariadbBackupHost(Command):
"""Show the host that 'osism apply mariadb-backup' writes archives to.

The value is resolved by the mariadb role itself, in a read-only play on
the kolla-ansible worker, because its default, the shard grouping it
depends on and the usual override are all outside the inventory.
"""

def get_parser(self, prog_name):
parser = super(MariadbBackupHost, self).get_parser(prog_name)
parser.add_argument(
"--format",
default="table",
choices=["table", "script"],
help="Output type; script prints the bare host name",
)
parser.add_argument(
"--timeout",
default=300,
type=int,
help="Seconds to wait for the play",
)
return parser

def take_action(self, parsed_args):
from celery.exceptions import TimeoutError as CeleryTimeoutError

from osism.tasks import AnsibleFailure, kolla

utils.check_task_lock_and_exit()

t = kolla.run.delay("kolla", "mariadb-backup-host", [], publish=False)
try:
output = t.get(timeout=parsed_args.timeout)
except AnsibleFailure as exc:
logger.error(
f"{exc}. 'osism apply -e kolla mariadb-backup-host' shows "
"the full output."
)
return 1
except CeleryTimeoutError:
logger.error(f"Timeout while waiting for task {t.task_id}.")
# Unsubscribe now; left pending, the result is released during
# interpreter shutdown and Celery prints a traceback.
t.forget()
return 1

match = BACKUP_HOST_PATTERN.search(output)
if not match:
logger.error("The play did not report a backup host.")
return 1

hosts = [host for host in match.group(1).split(",") if host]
if not hosts:
# kolla skips the backup silently in this case.
resolved = match.group(2).replace(",", ", ")
logger.error(
f"No backup would be taken: no resolved backup host "
f"({resolved}) is in the default MariaDB shard."
)
return 1

if parsed_args.format == "script":
print("\n".join(hosts))
else:
print(
tabulate([[host] for host in hosts], headers=["Host"], tablefmt="psql")
)

return


class States(Command):
def get_parser(self, prog_name):
parser = super(States, self).get_parser(prog_name)
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ osism.commands:
get facts = osism.commands.get:Facts
get hosts = osism.commands.get:Hosts
get hostvars = osism.commands.get:Hostvars
get mariadb-backup-host = osism.commands.get:MariadbBackupHost
get logs ansible = osism.commands.log:Ansible
get logs container = osism.commands.log:Container
get logs file = osism.commands.log:File
Expand Down
102 changes: 102 additions & 0 deletions tests/unit/commands/test_get.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,3 +420,105 @@ def test_states_prints_nothing_without_cache_entry(monkeypatch, capsys):
cmd.take_action(parsed_args)

assert capsys.readouterr().out == ""


# --- MariadbBackupHost.take_action ---


def _backup_line(selected, resolved):
return (
"ok: [testbed-node-0] => {\n"
f' "msg": "mariadb_backup_host={selected} resolved={resolved}"\n'
"}\n"
)


def _run_backup_host(args, output=None, side_effect=None):
cmd = _make(get.MariadbBackupHost)
parsed_args = cmd.get_parser("test").parse_args(args)

task = MagicMock()
if side_effect is not None:
task.get.side_effect = side_effect
else:
task.get.return_value = output

with patch("osism.commands.get.utils.check_task_lock_and_exit"), patch(
"osism.tasks.kolla.run"
) as mock_run:
mock_run.delay.return_value = task
result = cmd.take_action(parsed_args)

return result, mock_run


def test_mariadb_backup_host_runs_the_play_without_publishing(capsys):
result, mock_run = _run_backup_host(
[], output=_backup_line("testbed-node-0", "testbed-node-0")
)

assert not result
mock_run.delay.assert_called_once_with(
"kolla", "mariadb-backup-host", [], publish=False
)
assert "testbed-node-0" in capsys.readouterr().out


def test_mariadb_backup_host_script_format_prints_bare_host(capsys):
# Colour codes as the worker emits them must not leak into the host name.
output = "\x1b[0;32m" + _backup_line("testbed-node-1", "testbed-node-1") + "\x1b[0m"

result, _ = _run_backup_host(["--format", "script"], output=output)

assert not result
assert capsys.readouterr().out == "testbed-node-1\n"


def test_mariadb_backup_host_ignores_other_shards(capsys):
# Every shard resolves a host; only the default shard's is backed up.
output = _backup_line("db-0", "db-0,db-shard1-0")

result, _ = _run_backup_host(["--format", "script"], output=output)

assert not result
assert capsys.readouterr().out == "db-0\n"


def test_mariadb_backup_host_fails_when_no_backup_would_be_taken(loguru_logs, capsys):
result, _ = _run_backup_host([], output=_backup_line("", "db-shard1-0"))

assert result == 1
assert capsys.readouterr().out == ""
assert any(
r["level"] == "ERROR" and "db-shard1-0" in r["message"] for r in loguru_logs
)


def test_mariadb_backup_host_fails_without_a_report(loguru_logs):
result, _ = _run_backup_host([], output="PLAY RECAP\n")

assert result == 1
assert any("did not report a backup host" in r["message"] for r in loguru_logs)


def test_mariadb_backup_host_fails_when_the_play_fails(loguru_logs):
from osism.tasks import AnsibleFailure

result, _ = _run_backup_host(
[], side_effect=AnsibleFailure("kolla-ansible play failed with rc 1")
)

assert result == 1
assert any("osism apply -e kolla" in r["message"] for r in loguru_logs)


def test_mariadb_backup_host_fails_on_timeout(loguru_logs):
from celery.exceptions import TimeoutError as CeleryTimeoutError

result, mock_run = _run_backup_host(
["--timeout", "5"], side_effect=CeleryTimeoutError()
)

assert result == 1
assert any("Timeout" in r["message"] for r in loguru_logs)
mock_run.delay.return_value.forget.assert_called_once_with()