diff --git a/charms/openstack-hypervisor/charmcraft.yaml b/charms/openstack-hypervisor/charmcraft.yaml index 2b420416..9a08b7eb 100644 --- a/charms/openstack-hypervisor/charmcraft.yaml +++ b/charms/openstack-hypervisor/charmcraft.yaml @@ -58,6 +58,10 @@ actions: type: string description: IP address to use for service configuration additionalProperties: false + list-nics: + description: | + List host NICS, and which one are candidates for use as external NIC. + additionalProperties: false requires: amqp: diff --git a/charms/openstack-hypervisor/src/charm.py b/charms/openstack-hypervisor/src/charm.py index 77f7545a..64ff4cb4 100755 --- a/charms/openstack-hypervisor/src/charm.py +++ b/charms/openstack-hypervisor/src/charm.py @@ -27,6 +27,7 @@ import os import secrets import socket import string +import subprocess from typing import ( List, Optional, @@ -173,6 +174,10 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm): self.on.set_hypervisor_local_settings_action, self._set_hypervisor_local_settings_action, ) + self.framework.observe( + self.on.list_nics_action, + self._list_nics_action, + ) self.framework.observe( self.on.install, self._on_install, @@ -320,6 +325,39 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm): if new_snap_settings: self.set_snap_data(new_snap_settings) + def _list_nics_action(self, event: ActionEvent): + """Run list_nics action.""" + cache = self.get_snap_cache() + hypervisor = cache["openstack-hypervisor"] + + if not hypervisor.present: + event.fail("Hypervisor is not installed") + return + + process = subprocess.run( + [ + "snap", + "run", + "openstack-hypervisor", + "--verbose", + "list-nics", + "--format", + "json", + ], + capture_output=True, + ) + + stderr = process.stderr.decode("utf-8") + logger.debug("logs: %s", stderr) + stdout = process.stdout.decode("utf-8") + logger.debug("stdout: %s", stdout) + if process.returncode != 0: + event.fail(stderr) + return + + # cli returns a json dict with keys "nics" and "candidate" + event.set_results({"result": stdout}) + def ensure_services_running(self): """Ensure systemd services running.""" # This should taken care of by the snap diff --git a/charms/openstack-hypervisor/tests/unit/test_charm.py b/charms/openstack-hypervisor/tests/unit/test_charm.py index 3ea5efee..5441c46e 100644 --- a/charms/openstack-hypervisor/tests/unit/test_charm.py +++ b/charms/openstack-hypervisor/tests/unit/test_charm.py @@ -15,11 +15,14 @@ """Tests for Openstack hypervisor charm.""" import base64 +import json from unittest.mock import ( MagicMock, ) import charm +import ops +import ops.testing import ops_sunbeam.test_utils as test_utils @@ -35,7 +38,13 @@ class _HypervisorOperatorCharm(charm.HypervisorOperatorCharm): class TestCharm(test_utils.CharmTestCase): """Test charm to test relations.""" - PATCHES = ["socket", "snap", "get_local_ip_by_default_route", "os"] + PATCHES = [ + "socket", + "snap", + "get_local_ip_by_default_route", + "os", + "subprocess", + ] def setUp(self): """Setup OpenStack Hypervisor tests.""" @@ -277,3 +286,53 @@ class TestCharm(test_utils.CharmTestCase): "masakari.enable": True, } hypervisor_snap_mock.set.assert_any_call(expect_settings, typed=True) + + def test_list_nics_snap_not_installed(self): + """Check action raises ActionFailed if snap is not installed.""" + self.harness.begin() + hypervisor_snap_mock = MagicMock() + hypervisor_snap_mock.present = False + self.snap.SnapCache.return_value = { + "openstack-hypervisor": hypervisor_snap_mock + } + with self.assertRaises(ops.testing.ActionFailed): + self.harness.run_action("list-nics") + + def test_list_nics(self): + """Check action returns nics.""" + self.harness.begin() + hypervisor_snap_mock = MagicMock() + hypervisor_snap_mock.present = True + self.snap.SnapCache.return_value = { + "openstack-hypervisor": hypervisor_snap_mock + } + subprocess_run_mock = MagicMock() + subprocess_run_mock.return_value = MagicMock( + stdout=bytes( + json.dumps({"nics": ["eth0", "eth1"], "candidates": ["eth2"]}), + "utf-8", + ), + stderr=b"yes things went well", + returncode=0, + ) + self.subprocess.run = subprocess_run_mock + action_output = self.harness.run_action("list-nics") + assert "candidates" in action_output.results["result"] + + def test_list_nics_error(self): + """Check action raises ActionFailed if subprocess returns non-zero.""" + self.harness.begin() + hypervisor_snap_mock = MagicMock() + hypervisor_snap_mock.present = True + self.snap.SnapCache.return_value = { + "openstack-hypervisor": hypervisor_snap_mock + } + subprocess_run_mock = MagicMock() + subprocess_run_mock.return_value = MagicMock( + stdout=b"", + stderr=b"things did not go well", + returncode=1, + ) + self.subprocess.run = subprocess_run_mock + with self.assertRaises(ops.testing.ActionFailed): + self.harness.run_action("list-nics")