diff --git a/charms/openstack-hypervisor/pyproject.toml b/charms/openstack-hypervisor/pyproject.toml index 2edc519a..30821404 100644 --- a/charms/openstack-hypervisor/pyproject.toml +++ b/charms/openstack-hypervisor/pyproject.toml @@ -1,3 +1,6 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + # Testing tools configuration [tool.coverage.run] branch = true @@ -11,23 +14,26 @@ log_cli_level = "INFO" # Formatting tools configuration [tool.black] -line-length = 99 -target-version = ["py38"] +line-length = 79 [tool.isort] -line_length = 99 profile = "black" +multi_line_output = 3 +force_grid_wrap = true # Linting tools configuration [tool.flake8] -max-line-length = 99 +max-line-length = 79 max-doc-length = 99 max-complexity = 10 exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] select = ["E", "W", "F", "C", "N", "R", "D", "H"] # Ignore W503, E501 because using black creates errors with this # Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107"] -# D100, D101, D102, D103: Ignore missing docstrings in tests -per-file-ignores = ["tests/*:D100,D101,D102,D103,D104"] +ignore = ["W503", "E501", "D107", "E402"] +per-file-ignores = [] docstring-convention = "google" +# Check for properly formatted copyright header in each file +copyright-check = "True" +copyright-author = "Canonical Ltd." +copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/openstack-hypervisor/src/charm.py b/charms/openstack-hypervisor/src/charm.py index 3b358fc4..ac84f915 100755 --- a/charms/openstack-hypervisor/src/charm.py +++ b/charms/openstack-hypervisor/src/charm.py @@ -26,7 +26,9 @@ import os import secrets import socket import string -from typing import List +from typing import ( + List, +) import charms.operator_libs_linux.v2.snap as snap import ops.framework @@ -38,11 +40,19 @@ from charms.ceilometer_k8s.v0.ceilometer_service import ( CeilometerConfigChangedEvent, CeilometerServiceGoneAwayEvent, ) -from charms.grafana_agent.v0.cos_agent import COSAgentProvider -from ops.charm import ActionEvent -from ops.main import main +from charms.grafana_agent.v0.cos_agent import ( + COSAgentProvider, +) +from ops.charm import ( + ActionEvent, +) +from ops.main import ( + main, +) -from utils import get_local_ip_by_default_route +from utils import ( + get_local_ip_by_default_route, +) logger = logging.getLogger(__name__) @@ -61,7 +71,9 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm): self._state.set_default(metadata_secret="") self.enable_monitoring = self.check_relation_exists("cos-agent") # Enable telemetry when ceilometer-service relation is joined - self.enable_telemetry = self.check_relation_exists("ceilometer-service") + self.enable_telemetry = self.check_relation_exists( + "ceilometer-service" + ) self.framework.observe( self.on.set_hypervisor_local_settings_action, self._set_hypervisor_local_settings_action, @@ -111,11 +123,13 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm): ) handlers.append(self.ovsdb_cms) if self.can_add_handler("ceilometer-service", handlers): - self.ceilometer = sunbeam_rhandlers.CeilometerServiceRequiresHandler( - self, - "ceilometer-service", - self.handle_ceilometer_events, - "ceilometer-service" in self.mandatory_relations, + self.ceilometer = ( + sunbeam_rhandlers.CeilometerServiceRequiresHandler( + self, + "ceilometer-service", + self.handle_ceilometer_events, + "ceilometer-service" in self.mandatory_relations, + ) ) handlers.append(self.ceilometer) handlers = super().get_relation_handlers(handlers) @@ -199,9 +213,14 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm): hypervisor = cache["openstack-hypervisor"] if not hypervisor.present: - hypervisor.ensure(snap.SnapState.Latest, channel=config("snap-channel")) + hypervisor.ensure( + snap.SnapState.Latest, channel=config("snap-channel") + ) except snap.SnapError as e: - logger.error("An exception occurred when installing charmcraft. Reason: %s", e.message) + logger.error( + "An exception occurred when installing charmcraft. Reason: %s", + e.message, + ) def configure_unit(self, event) -> None: """Run configuration on this unit.""" @@ -212,13 +231,16 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm): local_ip = get_local_ip_by_default_route() try: contexts = self.contexts() - sb_connection_strs = list(contexts.ovsdb_cms.db_ingress_sb_connection_strs) + sb_connection_strs = list( + contexts.ovsdb_cms.db_ingress_sb_connection_strs + ) if not sb_connection_strs: raise AttributeError(name="ovsdb southbound ingress string") snap_data = { "compute.cpu-mode": "host-model", - "compute.spice-proxy-address": config("ip-address") or local_ip, + "compute.spice-proxy-address": config("ip-address") + or local_ip, "compute.virt-type": "kvm", "credentials.ovn-metadata-proxy-shared-secret": self.metadata_secret(), "identity.admin-role": contexts.identity_credentials.admin_role, @@ -236,11 +258,17 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm): "network.dns-servers": config("dns-servers"), "network.enable-gateway": config("enable-gateway"), "network.external-bridge": config("external-bridge"), - "network.external-bridge-address": config("external-bridge-address") + "network.external-bridge-address": config( + "external-bridge-address" + ) or "10.20.20.1/24", "network.ip-address": config("ip-address") or local_ip, - "network.ovn-key": base64.b64encode(contexts.certificates.key.encode()).decode(), - "network.ovn-cert": base64.b64encode(contexts.certificates.cert.encode()).decode(), + "network.ovn-key": base64.b64encode( + contexts.certificates.key.encode() + ).decode(), + "network.ovn-cert": base64.b64encode( + contexts.certificates.cert.encode() + ).decode(), "network.ovn-cacert": base64.b64encode( contexts.certificates.ca_cert.encode() ).decode(), @@ -252,7 +280,9 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm): "monitoring.enable": self.enable_monitoring, } except AttributeError as e: - raise sunbeam_guard.WaitingExceptionError("Data missing: {}".format(e.name)) + raise sunbeam_guard.WaitingExceptionError( + "Data missing: {}".format(e.name) + ) # Handle optional config contexts try: if contexts.ceph_access.uuid: diff --git a/charms/openstack-hypervisor/src/utils.py b/charms/openstack-hypervisor/src/utils.py index 94631852..60a93d0d 100644 --- a/charms/openstack-hypervisor/src/utils.py +++ b/charms/openstack-hypervisor/src/utils.py @@ -18,7 +18,9 @@ import logging -from typing import Optional +from typing import ( + Optional, +) import netifaces @@ -50,7 +52,9 @@ def _get_default_gw_iface_fallback() -> Optional[str]: # contents to line up. This is parsing the /proc/net/route and creating a set of # entries. Each entry is a dict where the keys are table header and the values # are the values in the table rows. - header = [col.strip().lower() for col in contents[0].split("\t") if col] + header = [ + col.strip().lower() for col in contents[0].split("\t") if col + ] for row in contents[1:]: cells = [col.strip() for col in row.split("\t") if col] entries.append(dict(zip(header, cells))) diff --git a/charms/openstack-hypervisor/tests/integration/test_charm.py b/charms/openstack-hypervisor/tests/integration/test_charm.py index 0acda5e2..06ca72f0 100644 --- a/charms/openstack-hypervisor/tests/integration/test_charm.py +++ b/charms/openstack-hypervisor/tests/integration/test_charm.py @@ -1,14 +1,32 @@ #!/usr/bin/env python3 -# Copyright 2023 Liam -# See LICENSE file for licensing details. + +# Copyright 2023 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Integration test for openstack hypervisor charm.""" import asyncio import logging -from pathlib import Path +from pathlib import ( + Path, +) import pytest import yaml -from pytest_operator.plugin import OpsTest +from pytest_operator.plugin import ( + OpsTest, +) logger = logging.getLogger(__name__) @@ -24,12 +42,21 @@ async def test_build_and_deploy(ops_test: OpsTest): """ # Build and deploy charm from local source folder charm = await ops_test.build_charm(".") - resources = {"httpbin-image": METADATA["resources"]["httpbin-image"]["upstream-source"]} + resources = { + "httpbin-image": METADATA["resources"]["httpbin-image"][ + "upstream-source" + ] + } # Deploy the charm and wait for active/idle status await asyncio.gather( - ops_test.model.deploy(await charm, resources=resources, application_name=APP_NAME), + ops_test.model.deploy( + await charm, resources=resources, application_name=APP_NAME + ), ops_test.model.wait_for_idle( - apps=[APP_NAME], status="active", raise_on_blocked=True, timeout=1000 + apps=[APP_NAME], + status="active", + raise_on_blocked=True, + timeout=1000, ), ) diff --git a/charms/openstack-hypervisor/tests/unit/test_charm.py b/charms/openstack-hypervisor/tests/unit/test_charm.py index 3344f9bf..5e3655f3 100644 --- a/charms/openstack-hypervisor/tests/unit/test_charm.py +++ b/charms/openstack-hypervisor/tests/unit/test_charm.py @@ -16,7 +16,9 @@ import base64 import json -from unittest import mock +from unittest import ( + mock, +) import ops_sunbeam.test_utils as test_utils @@ -33,6 +35,8 @@ class _HypervisorOperatorCharm(charm.HypervisorOperatorCharm): class TestCharm(test_utils.CharmTestCase): + """Test charm to test relations.""" + PATCHES = ["socket", "snap", "get_local_ip_by_default_route", "os"] def setUp(self): @@ -48,6 +52,7 @@ class TestCharm(test_utils.CharmTestCase): self.addCleanup(self.harness.cleanup) def initial_setup(self): + """Setting up relations.""" rel_id = self.harness.add_relation("certificates", "vault") self.harness.add_relation_unit(rel_id, "vault/0") self.harness.update_config({"snap-channel": "essex/stable"}) @@ -80,7 +85,9 @@ class TestCharm(test_utils.CharmTestCase): self.harness.add_relation_unit(ceph_rel_id, "cinder-ceph/0") credentials_content = {"uuid": "ddd", "key": "eee"} - credentials_id = self.harness.add_model_secret("cinder-ceph", credentials_content) + credentials_id = self.harness.add_model_secret( + "cinder-ceph", credentials_content + ) self.harness.grant_secret(credentials_id, self.harness.charm.app.name) self.harness.update_relation_data( @@ -95,20 +102,28 @@ class TestCharm(test_utils.CharmTestCase): hypervisor_snap_mock = mock.MagicMock() hypervisor_snap_mock.present = False self.snap.SnapState.Latest = "latest" - self.snap.SnapCache.return_value = {"openstack-hypervisor": hypervisor_snap_mock} + self.snap.SnapCache.return_value = { + "openstack-hypervisor": hypervisor_snap_mock + } self.socket.getfqdn.return_value = "test.local" self.initial_setup() self.harness.set_leader() - hypervisor_snap_mock.ensure.assert_any_call("latest", channel="essex/stable") + hypervisor_snap_mock.ensure.assert_any_call( + "latest", channel="essex/stable" + ) test_utils.add_complete_amqp_relation(self.harness) test_utils.add_complete_identity_credentials_relation(self.harness) metadata = self.harness.charm.metadata_secret() - ovn_cacert = test_utils.TEST_CA + "\n" + "\n".join(test_utils.TEST_CHAIN) + ovn_cacert = ( + test_utils.TEST_CA + "\n" + "\n".join(test_utils.TEST_CHAIN) + ) ovn_cacert = base64.b64encode(ovn_cacert.encode()).decode() private_key = base64.b64encode( self.harness.charm.contexts().certificates.key.encode() ).decode() - certificate = base64.b64encode(test_utils.TEST_SERVER_CERT.encode()).decode() + certificate = base64.b64encode( + test_utils.TEST_SERVER_CERT.encode() + ).decode() expect_settings = { "compute.cpu-mode": "host-model", "compute.spice-proxy-address": "10.0.0.10", @@ -163,27 +178,37 @@ class TestCharm(test_utils.CharmTestCase): # Add ceilometer-service relation self.harness.add_relation( - "ceilometer-service", "ceilometer", app_data={"telemetry-secret": "FAKE_SECRET"} + "ceilometer-service", + "ceilometer", + app_data={"telemetry-secret": "FAKE_SECRET"}, ) self.get_local_ip_by_default_route.return_value = "10.0.0.10" hypervisor_snap_mock = mock.MagicMock() hypervisor_snap_mock.present = False self.snap.SnapState.Latest = "latest" - self.snap.SnapCache.return_value = {"openstack-hypervisor": hypervisor_snap_mock} + self.snap.SnapCache.return_value = { + "openstack-hypervisor": hypervisor_snap_mock + } self.socket.getfqdn.return_value = "test.local" self.initial_setup() self.harness.set_leader() - hypervisor_snap_mock.ensure.assert_any_call("latest", channel="essex/stable") + hypervisor_snap_mock.ensure.assert_any_call( + "latest", channel="essex/stable" + ) test_utils.add_complete_amqp_relation(self.harness) test_utils.add_complete_identity_credentials_relation(self.harness) metadata = self.harness.charm.metadata_secret() - ovn_cacert = test_utils.TEST_CA + "\n" + "\n".join(test_utils.TEST_CHAIN) + ovn_cacert = ( + test_utils.TEST_CA + "\n" + "\n".join(test_utils.TEST_CHAIN) + ) ovn_cacert = base64.b64encode(ovn_cacert.encode()).decode() private_key = base64.b64encode( self.harness.charm.contexts().certificates.key.encode() ).decode() - certificate = base64.b64encode(test_utils.TEST_SERVER_CERT.encode()).decode() + certificate = base64.b64encode( + test_utils.TEST_SERVER_CERT.encode() + ).decode() expect_settings = { "compute.cpu-mode": "host-model", "compute.spice-proxy-address": "10.0.0.10",