diff --git a/openstack_hypervisor/api.py b/openstack_hypervisor/api.py index b7d32a7..6dbf493 100644 --- a/openstack_hypervisor/api.py +++ b/openstack_hypervisor/api.py @@ -31,6 +31,8 @@ "network": model.NetworkConfig, "node": model.NodeConfig, "logging": model.LoggingConfig, + "telemetry": model.TelemetryConfig, + "monitoring": model.MonitoringConfig, } @@ -138,6 +140,18 @@ async def update_logging(config: model.LoggingConfig): return _update_settings("logging", config) +@app.patch("/settings/telemetry") +async def update_telemetry(config: model.TelemetryConfig): + """Updates telemetry section settings.""" + return _update_settings("telemetry", config) + + +@app.patch("/settings/monitoring") +async def update_monitoring(config: model.MonitoringConfig): + """Updates monitoring section settings.""" + return _update_settings("monitoring", config) + + @app.post("/reset") async def reset_config(): """Reset all configs to default.""" diff --git a/openstack_hypervisor/hooks.py b/openstack_hypervisor/hooks.py index 0e51bb6..9cb6225 100644 --- a/openstack_hypervisor/hooks.py +++ b/openstack_hypervisor/hooks.py @@ -70,6 +70,7 @@ Path("etc/neutron/neutron.conf.d"), Path("etc/ssl/certs"), Path("etc/ssl/private"), + Path("etc/ceilometer"), # log Path("log/libvirt/qemu"), Path("log/ovn"), @@ -91,6 +92,22 @@ Path("lib/neutron"), Path("run/hypervisor-config"), ] +SECRET_XML = string.Template( + """ + + $uuid + + client.cinder-ceph secret + + +""" +) + +# As defined in the snap/snapcraft.yaml +MONITORING_SERVICES = [ + "libvirt-exporter", + "ovs-exporter", +] def _generate_secret(length: int = DEFAULT_SECRET_LENGTH) -> str: @@ -223,6 +240,9 @@ def _get_local_ip_by_default_route() -> str: "compute.virt-type": "auto", "compute.cpu-models": UNSET, "compute.spice-proxy-address": _get_local_ip_by_default_route, # noqa: F821 + "compute.rbd_user": "nova", + "compute.rbd_secret_uuid": UNSET, + "compute.rbd_key": UNSET, # Neutron "network.physnet-name": "physnet1", "network.external-bridge": "br-ex", @@ -236,11 +256,16 @@ def _get_local_ip_by_default_route() -> str: "network.enable-gateway": False, "network.ip-address": _get_local_ip_by_default_route, # noqa: F821 "network.external-nic": UNSET, + # Monitoring + "monitoring.enable": False, # General "logging.debug": False, "node.fqdn": socket.getfqdn, "node.ip-address": _get_local_ip_by_default_route, # noqa: F821 # TLS + # Telemetry + "telemetry.enable": False, + "telemetry.publisher-secret": UNSET, } @@ -256,6 +281,12 @@ def _get_local_ip_by_default_route() -> str: "network", ], "neutron-ovn-metadata-agent": ["credentials", "network", "node", "network.ovn_key"], + "ceilometer-compute-agent": [ + "identity.password", + "identity.username", + "identity", + "rabbitmq.url", + ], } @@ -335,6 +366,14 @@ def _context_compat(context: Dict[str, Any]) -> Dict[str, Any]: Path("etc/openvswitch/system-id.conf"): { "template": "system-id.conf.j2", }, + Path("etc/ceilometer/ceilometer.conf"): { + "template": "ceilometer.conf.j2", + "services": ["ceilometer-compute-agent"], + }, + Path("etc/ceilometer/polling.yaml"): { + "template": "polling.yaml.j2", + "services": ["ceilometer-compute-agent"], + }, } @@ -899,7 +938,73 @@ def _is_hw_virt_supported() -> bool: return False -def _configure_kvm(snap) -> None: +def _set_secret(conn, secret_uuid: str, secret_value: str) -> None: + """Set the ceph access secret in libvirt.""" + logging.info(f"Setting secret {secret_uuid}") + new_secret = conn.secretDefineXML(SECRET_XML.substitute(uuid=secret_uuid)) + # nova assumes the secret is raw and always encodes it *1, so decode it + # before storing it. + # *1 https://opendev.org/openstack/nova/src/branch/stable/2023.1/nova/ + # virt/libvirt/imagebackend.py#L1110 + new_secret.setValue(base64.b64decode(secret_value)) + + +def _get_libvirt(): + # Lazy import libvirt otherwise snap will not build + import libvirt + + return libvirt + + +def _ensure_secret(secret_uuid: str, secret_value: str) -> None: + """Ensure libvirt has the ceph access secret with the correct value.""" + libvirt = _get_libvirt() + conn = libvirt.open("qemu:///system") + # Check if secret exists + if secret_uuid in conn.listSecrets(): + logging.info(f"Found secret {secret_uuid}") + # check secret matches + secretobj = conn.secretLookupByUUIDString(secret_uuid) + try: + secret = secretobj.value() + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_SECRET: + logging.info(f"Secret {secret_uuid} has no value.") + secret = None + else: + raise + # Secret is stored raw so encode it before comparison. + if secret == base64.b64encode(secret_value.encode()): + logging.info(f"Secret {secret_uuid} has desired value.") + else: + logging.info(f"Secret {secret_uuid} has wrong value, replacing.") + secretobj.undefine() + _set_secret(conn, secret_uuid, secret_value) + else: + logging.info(f"Secret {secret_uuid} not found, creating.") + _set_secret(conn, secret_uuid, secret_value) + + +def _configure_ceph(snap) -> None: + """Configure ceph client. + + :param snap: the snap reference + :type snap: Snap + :return: None + """ + logging.info("Configuring ceph access") + context = ( + snap.config.get_options( + "compute", + ) + .as_dict() + .get("compute") + ) + if all(k in context for k in ("rbd-key", "rbd-secret-uuid")): + _ensure_secret(context["rbd-secret-uuid"], context["rbd-key"]) + + +def _configure_kvm(snap: Snap) -> None: """Configure KVM hardware virtualization. :param snap: the snap reference @@ -919,6 +1024,25 @@ def _configure_kvm(snap) -> None: snap.config.set({"compute.virt-type": "qemu"}) +def _configure_monitoring_services(snap: Snap) -> None: + """Configure all the monitoring services. + + :param snap: the snap reference + :type snap: Snap + :return: None + """ + services = snap.services.list() + enable_monitoring = snap.config.get("monitoring.enable") + if enable_monitoring: + logging.info("Enabling all exporter services.") + for service in MONITORING_SERVICES: + services[service].start(enable=True) + else: + logging.info("Disabling all exporter services.") + for service in MONITORING_SERVICES: + services[service].stop(disable=True) + + def services() -> List[str]: """List of services managed by hooks.""" return sorted(list(set([w for v in TEMPLATES.values() for w in v.get("services", [])]))) @@ -958,6 +1082,15 @@ def _services_not_ready(context: dict) -> List[str]: return sorted(list(set(not_ready))) +def _services_not_enabled_by_config(context: dict) -> List[str]: + """Check if services are enabled by configuration.""" + not_enabled = [] + if not context.get("telemetry", {}).get("enable"): + not_enabled.append("ceilometer-compute-agent") + + return not_enabled + + def configure(snap: Snap) -> None: """Runs the `configure` hook for the snap. @@ -984,6 +1117,8 @@ def configure(snap: Snap) -> None: "node", "rabbitmq", "credentials", + "telemetry", + "monitoring", ).as_dict() # Add some general snap path information @@ -997,6 +1132,7 @@ def configure(snap: Snap) -> None: context = _context_compat(context) logging.info(context) exclude_services = _services_not_ready(context) + exclude_services.extend(_services_not_enabled_by_config(context)) logging.warning(f"{exclude_services} are missing required config, stopping") services = snap.services.list() for service in exclude_services: @@ -1021,3 +1157,5 @@ def configure(snap: Snap) -> None: _configure_ovn_external_networking(snap) _configure_ovn_tls(snap) _configure_kvm(snap) + _configure_monitoring_services(snap) + _configure_ceph(snap) diff --git a/openstack_hypervisor/model.py b/openstack_hypervisor/model.py index bfc43ec..540b992 100644 --- a/openstack_hypervisor/model.py +++ b/openstack_hypervisor/model.py @@ -73,6 +73,9 @@ class ComputeConfig(BaseModel): virt_type: str = Field(alias="virt-type", default="auto") cpu_models: Optional[str] = Field(alias="cpu-models") spice_proxy_address: Optional[IPvAnyAddress] = Field(alias="spice-proxy-address") + rbd_user: Optional[str] = Field(alias="rbd-user", default="nova") + rbd_secret_uuid: Optional[str] = Field(alias="rbd-secret-uuid") + rbd_key: Optional[str] = Field(alias="rbd-key") class NetworkConfig(BaseModel): @@ -103,3 +106,16 @@ class LoggingConfig(BaseModel): """Data model for the logging configuration for the hypervisor.""" debug: bool = Field(default=False) + + +class TelemetryConfig(BaseModel): + """Data model for telemetry configuration settings.""" + + enable: bool = Field(default=False) + publisher_secret: Optional[str] = Field(alias="publisher-secret") + + +class MonitoringConfig(BaseModel): + """Data model for the monitoring configuration settings.""" + + enable: bool = Field(default=False) diff --git a/openstack_hypervisor/services.py b/openstack_hypervisor/services.py index c729586..2b9283e 100644 --- a/openstack_hypervisor/services.py +++ b/openstack_hypervisor/services.py @@ -35,6 +35,7 @@ class OpenStackService: conf_files = [] conf_dirs = [] + extra_args = [] executable = None @@ -70,6 +71,7 @@ def run(self, snap: Snap) -> int: cmd = [str(executable)] cmd.extend(args) + cmd.extend(self.extra_args) completed_process = subprocess.run(cmd) logging.info(f"Exiting with code {completed_process.returncode}") @@ -125,6 +127,21 @@ class NeutronOVNMetadataAgentService(OpenStackService): neutron_ovn_metadata_agent = partial(entry_point, NeutronOVNMetadataAgentService) +class CeilometerComputeAgentService(OpenStackService): + """A python service object used to run the ceilometer-agent-compute daemon.""" + + conf_files = [ + Path("etc/ceilometer/ceilometer.conf"), + ] + conf_dirs = [] + extra_args = ["--polling-namespaces", "compute"] + + executable = Path("usr/bin/ceilometer-polling") + + +ceilometer_compute_agent = partial(entry_point, CeilometerComputeAgentService) + + class OVSDBServerService: """A python service object used to run the ovsdb-server daemon.""" @@ -157,3 +174,56 @@ def run(self, snap: Snap) -> int: ovsdb_server = partial(entry_point, OVSDBServerService) + + +class OVSExporterService: + """A python service object used to run the ovs-exporter daemon.""" + + def run(self, snap: Snap) -> int: + """Runs the ovs-exporter service. + + Invoked when config monitoring is enable. + + :param snap: the snap context + :type snap: Snap + :return: exit code of the process + :rtype: int + """ + setup_logging(snap.paths.common / "ovs-exporter.log") + executable = snap.paths.snap / "bin" / "ovs-exporter" + listen_address = ":9475" + args = [ + f"-web.listen-address={listen_address}", + "-database.vswitch.file.data.path", + f"{snap.paths.common}/etc/openvswitch/conf.db", + "-database.vswitch.file.log.path", + f"{snap.paths.common}/log/openvswitch/ovsdb-server.log", + "-database.vswitch.file.pid.path", + f"{snap.paths.common}/run/openvswitch/ovsdb-server.pid", + "-database.vswitch.file.system.id.path", + f"{snap.paths.common}/etc/openvswitch/system-id.conf", + "-database.vswitch.name", + "Open_vSwitch", + "-database.vswitch.socket.remote", + "unix:" + f"{snap.paths.common}/run/openvswitch/db.sock", + "-service.ovncontroller.file.log.path", + f"{snap.paths.common}/log/ovn/ovn-controller.log", + "-service.ovncontroller.file.pid.path", + f"{snap.paths.common}/run/ovn/ovn-controller.pid", + "-service.vswitchd.file.log.path", + f"{snap.paths.common}/log/openvswitch/ovs-vswitchd.log", + "-service.vswitchd.file.pid.path", + f"{snap.paths.common}/run/openvswitch/ovs-vswitchd.pid", + "-system.run.dir", + f"{snap.paths.common}/run/openvswitch", + ] + cmd = [str(executable)] + cmd.extend(args) + + completed_process = subprocess.run(cmd) + + logging.info(f"Exiting with code {completed_process.returncode}") + return completed_process.returncode + + +ovs_exporter = partial(entry_point, OVSExporterService) diff --git a/setup.cfg b/setup.cfg index ad97be8..46a5d27 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,8 @@ console_scripts = nova-api-metadata-service = openstack_hypervisor.services:nova_api_metadata ovsdb-server-service = openstack_hypervisor.services:ovsdb_server neutron-ovn-metadata-agent-service = openstack_hypervisor.services:neutron_ovn_metadata_agent + ceilometer-compute-agent-service = openstack_hypervisor.services:ceilometer_compute_agent + ovs-exporter-service = openstack_hypervisor.services:ovs_exporter snaphelpers.hooks = configure = openstack_hypervisor.hooks:configure diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 76e4c2d..f6c6190 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -124,6 +124,7 @@ apps: - hardware-observe - hugepages-control - microstack-support + ovsdb-server: command: "bin/ovsdb-server-service" stop-command: usr/share/openvswitch/scripts/ovs-ctl --no-ovs-vswitchd stop @@ -233,6 +234,16 @@ apps: - firewall-control - microstack-support + ceilometer-compute-agent: + command: 'bin/ceilometer-compute-agent-service' + after: [libvirtd] + daemon: simple + plugs: + - network + - network-bind + - firewall-control + - microstack-support + hypervisor-config-service: command: 'bin/gunicorn openstack_hypervisor.api:app --workers 1 --worker-class uvicorn.workers.UvicornWorker --timeout 120 --bind unix:$SNAP_DATA/run/hypervisor-config/unix.socket' restart-condition: on-failure @@ -251,6 +262,28 @@ apps: listen-stream: $SNAP_DATA/run/hypervisor-config/unix.socket socket-mode: 0666 + libvirt-exporter: + daemon: simple + install-mode: disable + restart-condition: on-abnormal + command: 'bin/libvirt-exporter --web.listen-address :9177 --web.telemetry-path /metrics --libvirt.uri qemu:///system' + plugs: + - libvirt + - network-bind + + ovs-exporter: + command: 'bin/ovs-exporter-service' + plugs: + - network-bind + - openvswitch + - log-observe + - system-observe + - network-observe + - netlink-audit + - kernel-module-observe + daemon: simple + install-mode: disable + parts: kvm-support: plugin: nil @@ -258,6 +291,41 @@ parts: - on amd64: - msr-tools + ovs-exporter: + plugin: go + source: https://github.com/greenpau/ovs_exporter.git + source-tag: v1.0.7 + build-packages: + - gcc + - golang-go + override-build: | + GIT_COMMIT=$(git describe --dirty --always) + GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD -- | head -1) + BUILD_USER=snapcraft + BUILD_DATE=$(date +"%Y-%m-%d") + BINARY=ovs-exporter + VERBOSE=-v + PROJECT=ovs_exporter + PKG_DIR=pkg/ovs_exporter + mkdir -p ./bin/ + CGO_ENABLED=0 go build -o ./bin/${BINARY} \ + -ldflags="-w -s \ + -X github.com/prometheus/common/version.Version=$SNAPCRAFT_PROJECT_VERSION \ + -X github.com/prometheus/common/version.Revision=$GIT_COMMIT \ + -X github.com/prometheus/common/version.Branch=$GIT_BRANCH \ + -X github.com/prometheus/common/version.BuildUser=$BUILD_USER \ + -X github.com/prometheus/common/version.BuildDate=$BUILD_DATE \ + -X ${PROJECT}/${PKG_DIR}.appName=$BINARY \ + -X ${PROJECT}/${PKG_DIR}.appVersion=$SNAPCRAFT_PROJECT_VERSION \ + -X ${PROJECT}/${PKG_DIR}.gitBranch=$GIT_BRANCH \ + -X ${PROJECT}/${PKG_DIR}.gitCommit=$GIT_COMMIT \ + -X ${PROJECT}/${PKG_DIR}.buildUser=$BUILD_USER \ + -X ${PROJECT}/${PKG_DIR}.buildDate=$BUILD_DATE" \ + ./cmd/ovs_exporter/*.go + override-prime: | + snapcraftctl prime + install -D $SNAPCRAFT_PART_SRC/../build/bin/ovs-exporter bin/ovs-exporter + qemu: source: https://git.launchpad.net/ubuntu/+source/qemu source-type: git @@ -540,6 +608,7 @@ parts: openstack: plugin: nil stage-packages: + - ceilometer-agent-compute - python3-nova - python3-neutron - python3-os-vif @@ -575,6 +644,19 @@ parts: craftctl default snap-helpers write-hooks + libvirt-exporter: + plugin: go + source-tag: 2.3.3 + source-type: git + source: https://github.com/Tinkoff/libvirt-exporter + build-snaps: + - go + after: + - libvirt + build-environment: + - CGO_CFLAGS: "-I$CRAFT_STAGE/usr/include" + - CGO_LDFLAGS: "-L$CRAFT_STAGE/usr/lib" + slots: hypervisor-config: interface: content diff --git a/templates/ceilometer.conf.j2 b/templates/ceilometer.conf.j2 new file mode 100644 index 0000000..dd3095a --- /dev/null +++ b/templates/ceilometer.conf.j2 @@ -0,0 +1,52 @@ +# THIS FILE IS MANAGED BY THE SNAP - CHANGES WILL BE OVERWRITTEN +# Use $SNAP_COMMON/etc/ceilometer/ceilometer.conf.d for deployment specific +# configuration +[DEFAULT] +debug = {{ logging.debug }} + +# AMQP connection to RabbitMQ +transport_url = {{ rabbitmq.url }} + +[polling] +batch_size = 50 +cfg_file = {{ snap_common }}/etc/ceilometer/polling.yaml +pollsters_definitions_dirs = {{ snap_common }}/etc/ceilometer/pollsters.d + +{% if telemetry.publisher_secret -%} +[publisher] +telemetry_secret = {{ telemetry.publisher_secret }} +{%- endif %} + +[gnocchi] +filter_service_activity = False +archive_policy = low + +[keystone_authtoken] +auth_url = {{ identity.auth_url }} +auth_type = password +project_domain_name = {{ identity.project_domain_name }} +user_domain_name = {{ identity.user_domain_name }} +project_name = {{ identity.project_name }} +username = {{ identity.username }} +password = {{ identity.password }} +service_token_roles = {{ identity.admin_role }} +service_token_roles_required = True + +[service_user] +send_service_user_token = true +auth_type = password +auth_url = {{ identity.auth_url }} +project_domain_id = {{ identity.project_domain_id }} +user_domain_id = {{ identity.user_domain_id }} +project_name = {{ identity.project_name }} +username = {{ identity.username }} +password = {{ identity.password }} + +[service_credentials] +auth_type = password +auth_url = {{ identity.auth_url }} +project_domain_id = {{ identity.project_domain_id }} +user_domain_id = {{ identity.user_domain_id }} +project_name = {{ identity.project_name }} +username = {{ identity.username }} +password = {{ identity.password }} diff --git a/templates/nova.conf.j2 b/templates/nova.conf.j2 index 726f2db..e9b3e8a 100644 --- a/templates/nova.conf.j2 +++ b/templates/nova.conf.j2 @@ -33,6 +33,10 @@ cpu_mode = {{ compute.cpu_mode }} cpu_models = {{ compute.cpu_models }} {% endif %} +{% if compute.rbd_secret_uuid %} +rbd_user = {{ compute.rbd_user }} +rbd_secret_uuid = {{ compute.rbd_secret_uuid }} +{% endif %} [oslo_concurrency] # Oslo Concurrency lock path @@ -108,3 +112,8 @@ region_name = {{ identity.region_name }} project_name = {{ identity.project_name }} username = {{ identity.username }} password = {{ identity.password }} + ++{% if telemetry.enable -%} ++[oslo_messaging_notifications] ++driver = messagingv2 ++{%- endif %} diff --git a/templates/polling.yaml.j2 b/templates/polling.yaml.j2 new file mode 100644 index 0000000..0faf04b --- /dev/null +++ b/templates/polling.yaml.j2 @@ -0,0 +1,19 @@ +# THIS FILE IS MANAGED BY THE SNAP - CHANGES WILL BE OVERWRITTEN +{% if telemetry.enable -%} +--- +sources: + - name: some_pollsters + interval: 300 + meters: + - cpu + - cpu_l3_cache + - memory.usage + - network.incoming.bytes + - network.incoming.packets + - network.outgoing.bytes + - network.outgoing.packets + - disk.device.read.bytes + - disk.device.read.requests + - disk.device.write.bytes + - disk.device.write.requests +{%- endif %} diff --git a/tests/unit/test_hooks.py b/tests/unit/test_hooks.py index 593aee8..ff03b41 100644 --- a/tests/unit/test_hooks.py +++ b/tests/unit/test_hooks.py @@ -134,6 +134,7 @@ def test_configure_hook_exception(self, mocker, snap, os_makedirs, check_call): def test_services(self): """Test getting a list of managed services.""" assert hooks.services() == [ + "ceilometer-compute-agent", "libvirtd", "neutron-ovn-metadata-agent", "nova-api-metadata", @@ -162,12 +163,14 @@ def test_check_config_present(self): def test_services_not_ready(self, snap): config = {} assert hooks._services_not_ready(config) == [ + "ceilometer-compute-agent", "neutron-ovn-metadata-agent", "nova-api-metadata", "nova-compute", ] config["identity"] = {"username": "user", "password": "pass"} assert hooks._services_not_ready(config) == [ + "ceilometer-compute-agent", "neutron-ovn-metadata-agent", "nova-api-metadata", "nova-compute", @@ -188,6 +191,14 @@ def test_services_not_ready(self, snap): config["credentials"] = {"ovn_metadata_proxy_shared_secret": "secret"} assert hooks._services_not_ready(config) == [] + def test_services_not_enabled_by_config(self, snap): + config = {} + assert hooks._services_not_enabled_by_config(config) == [ + "ceilometer-compute-agent", + ] + config["telemetry"] = {"enable": True} + assert hooks._services_not_enabled_by_config(config) == [] + def test_list_bridge_ifaces(self, check_output): check_output.return_value = b"int1\nint2\n" assert hooks._list_bridge_ifaces("br1") == ["int1", "int2"] @@ -317,3 +328,70 @@ def test_del_external_nics_from_bridge(self, mocker): hooks._del_external_nics_from_bridge("br-ex") expect = [mock.call("br-ex", "eth0"), mock.call("br-ex", "eth1")] mock_del_interface_from_bridge.assert_has_calls(expect) + + def test_set_secret(self, mocker): + conn_mock = mocker.Mock() + secret_mock = mocker.Mock() + conn_mock.secretDefineXML.return_value = secret_mock + hooks._set_secret(conn_mock, "uuid1", "c2VjcmV0Cg==") + conn_mock.secretDefineXML.assert_called_once() + secret_mock.setValue.assert_called_once_with(b"secret\n") + + def test_ensure_secret_new_secret(self, mocker): + conn_mock = mocker.Mock() + mock_libvirt = mocker.Mock() + mock_get_libvirt = mocker.patch.object(hooks, "_get_libvirt") + mock_get_libvirt.return_value = mock_libvirt + mock_libvirt.open.return_value = conn_mock + mock_set_secret = mocker.patch.object(hooks, "_set_secret") + conn_mock.listSecrets.return_value = [] + hooks._ensure_secret("uuid1", "secret") + mock_set_secret.assert_called_once_with(conn_mock, "uuid1", "secret") + + def test_ensure_secret_secret_exists(self, mocker): + conn_mock = mocker.Mock() + mock_libvirt = mocker.Mock() + secret_mock = mocker.Mock() + secret_mock.value.return_value = b"c2VjcmV0" + mock_get_libvirt = mocker.patch.object(hooks, "_get_libvirt") + mock_get_libvirt.return_value = mock_libvirt + mock_libvirt.open.return_value = conn_mock + mock_set_secret = mocker.patch.object(hooks, "_set_secret") + conn_mock.listSecrets.return_value = ["uuid1"] + conn_mock.secretLookupByUUIDString.return_value = secret_mock + hooks._ensure_secret("uuid1", "secret") + assert not mock_set_secret.called + + def test_ensure_secret_secret_wrong_value(self, mocker): + conn_mock = mocker.Mock() + mock_libvirt = mocker.Mock() + secret_mock = mocker.Mock() + secret_mock.value.return_value = b"wrong" + mock_get_libvirt = mocker.patch.object(hooks, "_get_libvirt") + mock_get_libvirt.return_value = mock_libvirt + mock_libvirt.open.return_value = conn_mock + mock_set_secret = mocker.patch.object(hooks, "_set_secret") + conn_mock.listSecrets.return_value = ["uuid1"] + conn_mock.secretLookupByUUIDString.return_value = secret_mock + hooks._ensure_secret("uuid1", "secret") + mock_set_secret.assert_called_once_with(conn_mock, "uuid1", "secret") + + def test_ensure_secret_secret_missing_value(self, mocker): + class FakeError(Exception): + def get_error_code(self): + return 42 + + conn_mock = mocker.Mock() + mock_libvirt = mocker.Mock() + mock_libvirt.libvirtError = FakeError + mock_libvirt.VIR_ERR_NO_SECRET = 42 + secret_mock = mocker.Mock() + secret_mock.value.side_effect = FakeError() + mock_get_libvirt = mocker.patch.object(hooks, "_get_libvirt") + mock_get_libvirt.return_value = mock_libvirt + mock_libvirt.open.return_value = conn_mock + mock_set_secret = mocker.patch.object(hooks, "_set_secret") + conn_mock.listSecrets.return_value = ["uuid1"] + conn_mock.secretLookupByUUIDString.return_value = secret_mock + hooks._ensure_secret("uuid1", "secret") + mock_set_secret.assert_called_once_with(conn_mock, "uuid1", "secret")