From 49173f55ddf69ae313ddae6fbe18ad6eaddbfee0 Mon Sep 17 00:00:00 2001 From: James Page Date: Thu, 3 Nov 2022 15:17:03 +0100 Subject: [PATCH] General tidy for module ready for release. Refresh charm to drop release usage in ops-sunbeam. Drop surplus template fragments. Refresh unit tests. Tidy requirements.txt. Switch to black + other linters. Tidy docstrings across operator. Change-Id: I872da0c54dda857a4005b84905cb248d7a9782ae --- charms/nova-k8s/pyproject.toml | 39 +++ charms/nova-k8s/requirements.txt | 8 - charms/nova-k8s/src/charm.py | 272 +++++++++++------- .../src/templates/parts/section-database | 7 - .../src/templates/parts/section-federation | 10 - .../src/templates/parts/section-middleware | 6 - .../src/templates/parts/section-signing | 15 - charms/nova-k8s/tests/unit/__init__.py | 15 + charms/nova-k8s/tests/unit/test_nova_charm.py | 64 +++-- charms/nova-k8s/tox.ini | 41 ++- 10 files changed, 296 insertions(+), 181 deletions(-) create mode 100644 charms/nova-k8s/pyproject.toml delete mode 100644 charms/nova-k8s/src/templates/parts/section-database delete mode 100644 charms/nova-k8s/src/templates/parts/section-federation delete mode 100644 charms/nova-k8s/src/templates/parts/section-middleware delete mode 100644 charms/nova-k8s/src/templates/parts/section-signing diff --git a/charms/nova-k8s/pyproject.toml b/charms/nova-k8s/pyproject.toml new file mode 100644 index 00000000..2896bc05 --- /dev/null +++ b/charms/nova-k8s/pyproject.toml @@ -0,0 +1,39 @@ +# Copyright 2022 Canonical Ltd. +# See LICENSE file for licensing details. + +# Testing tools configuration +[tool.coverage.run] +branch = true + +[tool.coverage.report] +show_missing = true + +[tool.pytest.ini_options] +minversion = "6.0" +log_cli_level = "INFO" + +# Formatting tools configuration +[tool.black] +line-length = 79 + +[tool.isort] +profile = "black" +multi_line_output = 3 +force_grid_wrap = true + +# Linting tools configuration +[tool.flake8] +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", "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/nova-k8s/requirements.txt b/charms/nova-k8s/requirements.txt index b9b8fb9b..63e8a30a 100644 --- a/charms/nova-k8s/requirements.txt +++ b/charms/nova-k8s/requirements.txt @@ -16,12 +16,4 @@ lightkube lightkube-models ops git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam - -python-keystoneclient # keystone-k8s - git+https://opendev.org/openstack/charm-ops-interface-tls-certificates#egg=interface_tls_certificates - -# Note: Required for cinder-k8s, cinder-ceph-k8s, glance-k8s, nova-k8s -git+https://opendev.org/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client -# Charmhelpers is only present as interface_ceph_client uses it. -git+https://github.com/juju/charm-helpers.git#egg=charmhelpers diff --git a/charms/nova-k8s/src/charm.py b/charms/nova-k8s/src/charm.py index 4bebdd12..3d59fffe 100755 --- a/charms/nova-k8s/src/charm.py +++ b/charms/nova-k8s/src/charm.py @@ -1,4 +1,18 @@ #!/usr/bin/env python3 +# Copyright 2022 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. + """Nova Operator Charm. This charm provide Nova services as part of an OpenStack deployment @@ -6,19 +20,25 @@ This charm provide Nova services as part of an OpenStack deployment import logging import uuid -from typing import Callable, List, Mapping - -import ops.framework -from ops.main import main -from ops.pebble import ExecError - -import ops_sunbeam.charm as sunbeam_charm -import ops_sunbeam.core as sunbeam_core -import ops_sunbeam.container_handlers as sunbeam_chandlers -import ops_sunbeam.relation_handlers as sunbeam_rhandlers -import ops_sunbeam.config_contexts as sunbeam_ctxts +from typing import ( + Callable, + List, + Mapping, +) import charms.sunbeam_nova_compute_operator.v0.cloud_compute as cloud_compute +import ops.framework +import ops_sunbeam.charm as sunbeam_charm +import ops_sunbeam.config_contexts as sunbeam_ctxts +import ops_sunbeam.container_handlers as sunbeam_chandlers +import ops_sunbeam.core as sunbeam_core +import ops_sunbeam.relation_handlers as sunbeam_rhandlers +from ops.main import ( + main, +) +from ops.pebble import ( + ExecError, +) logger = logging.getLogger(__name__) @@ -31,23 +51,24 @@ class WSGINovaMetadataConfigContext(sunbeam_ctxts.ConfigContext): def context(self) -> dict: """WSGI configuration options.""" - log_svc_name = self.charm.service_name.replace('-', '_') + log_svc_name = self.charm.service_name.replace("-", "_") return { "name": self.charm.service_name, "public_port": 8775, "user": self.charm.service_user, "group": self.charm.service_group, - "wsgi_admin_script": '/usr/bin/nova-metadata-wsgi', - "wsgi_public_script": '/usr/bin/nova-metadata-wsgi', + "wsgi_admin_script": "/usr/bin/nova-metadata-wsgi", + "wsgi_public_script": "/usr/bin/nova-metadata-wsgi", "error_log": f"/var/log/apache2/{log_svc_name}_error.log", "custom_log": f"/var/log/apache2/{log_svc_name}_access.log", } class NovaSchedulerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): + """Pebble handler for Nova scheduler.""" def get_layer(self) -> dict: - """Nova Scheduler service + """Nova Scheduler service layer. :returns: pebble layer configuration for scheduler service :rtype: dict @@ -60,9 +81,9 @@ class NovaSchedulerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): "override": "replace", "summary": "Nova Scheduler", "command": "nova-scheduler", - "startup": "enabled" + "startup": "enabled", } - } + }, } def get_healthcheck_layer(self) -> dict: @@ -76,25 +97,27 @@ class NovaSchedulerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): "online": { "override": "replace", "level": "ready", - "exec": { - "command": "service nova-scheduler status" - } + "exec": {"command": "service nova-scheduler status"}, }, } } - def default_container_configs(self): + def default_container_configs( + self, + ) -> List[sunbeam_core.ContainerConfigFile]: + """Container configurations for handler.""" return [ sunbeam_core.ContainerConfigFile( - '/etc/nova/nova.conf', - 'nova', - 'nova')] + "/etc/nova/nova.conf", "nova", "nova" + ) + ] class NovaConductorPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): + """Pebble handler for Nova Conductor container.""" def get_layer(self): - """Nova Conductor service + """Nova Conductor service. :returns: pebble service layer configuration for conductor service :rtype: dict @@ -107,9 +130,9 @@ class NovaConductorPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): "override": "replace", "summary": "Nova Conductor", "command": "nova-conductor", - "startup": "enabled" + "startup": "enabled", } - } + }, } def get_healthcheck_layer(self) -> dict: @@ -122,19 +145,20 @@ class NovaConductorPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): "online": { "override": "replace", "level": "ready", - "exec": { - "command": "service nova-conductor status" - } + "exec": {"command": "service nova-conductor status"}, }, } } - def default_container_configs(self): + def default_container_configs( + self, + ) -> List[sunbeam_core.ContainerConfigFile]: + """Container configurations for handler.""" return [ sunbeam_core.ContainerConfigFile( - '/etc/nova/nova.conf', - 'nova', - 'nova')] + "/etc/nova/nova.conf", "nova", "nova" + ) + ] class CloudComputeRequiresHandler(sunbeam_rhandlers.RelationHandler): @@ -148,7 +172,9 @@ class CloudComputeRequiresHandler(sunbeam_rhandlers.RelationHandler): callback_f: Callable, mandatory: bool = False, ): - """Creates a new CloudComputeRequiresHandler that handles initial + """Constructor for CloudComputeRequiresHandler. + + Creates a new CloudComputeRequiresHandler that handles initial events from the relation and invokes the provided callbacks based on the event raised. @@ -175,11 +201,11 @@ class CloudComputeRequiresHandler(sunbeam_rhandlers.RelationHandler): ) self.framework.observe( compute_service.on.compute_nodes_connected, - self._compute_nodes_connected + self._compute_nodes_connected, ) self.framework.observe( compute_service.on.compute_nodes_ready, - self._compute_nodes_connected + self._compute_nodes_connected, ) return compute_service @@ -191,6 +217,7 @@ class CloudComputeRequiresHandler(sunbeam_rhandlers.RelationHandler): @property def ready(self) -> bool: + """Interface ready for use.""" return True @@ -199,20 +226,21 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): _state = ops.framework.StoredState() service_name = "nova-api" - wsgi_admin_script = '/usr/bin/nova-api-wsgi' - wsgi_public_script = '/usr/bin/nova-api-wsgi' - shared_metadata_secret_key = 'shared-metadata-secret' + wsgi_admin_script = "/usr/bin/nova-api-wsgi" + wsgi_public_script = "/usr/bin/nova-api-wsgi" + shared_metadata_secret_key = "shared-metadata-secret" mandatory_relations = { - 'database', - 'api-database', - 'cell-database', - 'amqp', - 'identity-service', - 'ingress-public', + "database", + "api-database", + "cell-database", + "amqp", + "identity-service", + "ingress-public", } @property def db_sync_cmds(self) -> List[List[str]]: + """DB sync commands for Nova operator.""" # we must provide the database connection for the cell database, # because the database credentials are different to the main database. # If we don't provide them: @@ -222,14 +250,29 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): # https://docs.openstack.org/nova/yoga/admin/cells.html#configuring-a-new-deployment cell_database = self.dbs["cell-database"].context()["connection"] return [ - ['sudo', '-u', 'nova', 'nova-manage', 'api_db', 'sync'], + ["sudo", "-u", "nova", "nova-manage", "api_db", "sync"], [ - 'sudo', '-u', 'nova', 'nova-manage', 'cell_v2', 'map_cell0', - '--database_connection', cell_database + "sudo", + "-u", + "nova", + "nova-manage", + "cell_v2", + "map_cell0", + "--database_connection", + cell_database, + ], + ["sudo", "-u", "nova", "nova-manage", "db", "sync"], + [ + "sudo", + "-u", + "nova", + "nova-manage", + "cell_v2", + "create_cell", + "--name", + "cell1", + "--verbose", ], - ['sudo', '-u', 'nova', 'nova-manage', 'db', 'sync'], - ['sudo', '-u', 'nova', 'nova-manage', 'cell_v2', 'create_cell', - '--name', 'cell1', '--verbose'], ] @property @@ -240,26 +283,30 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): @property def service_user(self) -> str: """Service user file and directory ownership.""" - return 'nova' + return "nova" @property def service_group(self) -> str: """Service group file and directory ownership.""" - return 'nova' + return "nova" @property def service_endpoints(self): + """Service endpoints for Nova.""" return [ { - 'service_name': 'nova', - 'type': 'compute', - 'description': "OpenStack Compute", - 'internal_url': f'{self.internal_url}/v2.1', - 'public_url': f'{self.public_url}/v2.1', - 'admin_url': f'{self.admin_url}/v2.1'}] + "service_name": "nova", + "type": "compute", + "description": "OpenStack Compute", + "internal_url": f"{self.internal_url}/v2.1", + "public_url": f"{self.public_url}/v2.1", + "admin_url": f"{self.admin_url}/v2.1", + } + ] @property def default_public_ingress_port(self): + """Default port for service ingress.""" return 8774 @property @@ -275,41 +322,43 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): "cell-database": "nova_cell0", } - def get_pebble_handlers(self): + def get_pebble_handlers( + self, + ) -> List[sunbeam_chandlers.ServicePebbleHandler]: + """Pebble handlers for operator.""" pebble_handlers = super().get_pebble_handlers() - pebble_handlers.extend([ - NovaSchedulerPebbleHandler( - self, - NOVA_SCHEDULER_CONTAINER, - 'nova-scheduler', - [], - self.template_dir, - self.openstack_release, - self.configure_charm), - NovaConductorPebbleHandler( - self, - NOVA_CONDUCTOR_CONTAINER, - 'nova-conductor', - [], - self.template_dir, - self.openstack_release, - self.configure_charm)]) + pebble_handlers.extend( + [ + NovaSchedulerPebbleHandler( + self, + NOVA_SCHEDULER_CONTAINER, + "nova-scheduler", + [], + self.template_dir, + self.configure_charm, + ), + NovaConductorPebbleHandler( + self, + NOVA_CONDUCTOR_CONTAINER, + "nova-conductor", + [], + self.template_dir, + self.configure_charm, + ), + ] + ) return pebble_handlers def get_relation_handlers( self, handlers: List[sunbeam_rhandlers.RelationHandler] = None ) -> List[sunbeam_rhandlers.RelationHandler]: - """ - - :param handlers: - :return: - """ + """Relation handlers for operator.""" handlers = super().get_relation_handlers(handlers or []) if self.can_add_handler("cloud-compute", handlers): self.compute_nodes = CloudComputeRequiresHandler( self, - 'cloud-compute', - self.model.config['region'], + "cloud-compute", + self.model.config["region"], self.register_compute_nodes, ) handlers.append(self.compute_nodes) @@ -322,7 +371,8 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): _cadapters.extend( [ WSGINovaMetadataConfigContext( - self, 'wsgi_nova_metadata', + self, + "wsgi_nova_metadata", ) ] ) @@ -334,8 +384,7 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): def set_shared_metadatasecret(self): """Store the shared metadata secret.""" - self.leader_set( - {self.shared_metadata_secret_key: str(uuid.uuid1())}) + self.leader_set({self.shared_metadata_secret_key: str(uuid.uuid1())}) def register_compute_nodes(self, event: ops.framework.EventBase) -> None: """Register compute nodes when the event is received. @@ -363,27 +412,33 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): # return self.compute_nodes.interface.set_controller_info( - region=self.model.config['region'], + region=self.model.config["region"], cross_az_attach=False, ) try: - logger.debug('Discovering hosts for cell1') - cell1_uuid = self.get_cell_uuid('cell1') - cmd = ['nova-manage', 'cell_v2', 'discover_hosts', '--cell_uuid', - cell1_uuid, '--verbose'] + logger.debug("Discovering hosts for cell1") + cell1_uuid = self.get_cell_uuid("cell1") + cmd = [ + "nova-manage", + "cell_v2", + "discover_hosts", + "--cell_uuid", + cell1_uuid, + "--verbose", + ] handler.execute(cmd, exception_on_error=True) except ExecError: - logger.exception('Failed to discover hosts for cell1') + logger.exception("Failed to discover hosts for cell1") raise def get_cell_uuid(self, cell, fatal=True): - """Returns the cell UUID from the name + """Returns the cell UUID from the name. :param cell: string cell name i.e. 'cell1' :returns: string cell uuid """ - logger.debug(f'listing cells for {cell}') + logger.debug(f"listing cells for {cell}") cells = self.get_cells() cell_info = cells.get(cell) if not cell_info: @@ -391,7 +446,7 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): raise Exception(f"Cell {cell} not found") return None - return cell_info['uuid'] + return cell_info["uuid"] def get_cells(self): """Returns the cells configured in the environment. @@ -401,31 +456,33 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): """ logger.info("Getting details of cells") cells = {} - cmd = ['sudo', 'nova-manage', 'cell_v2', 'list_cells', '--verbose'] + cmd = ["sudo", "nova-manage", "cell_v2", "list_cells", "--verbose"] handler = self.get_named_pebble_handler(NOVA_CONDUCTOR_CONTAINER) try: out = handler.execute(cmd, exception_on_error=True) except ExecError: - logger.exception('list_cells failed') + logger.exception("list_cells failed") raise - for line in out.split('\n'): - columns = line.split('|') + for line in out.split("\n"): + columns = line.split("|") if len(columns) < 2: continue columns = [c.strip() for c in columns] try: uuid.UUID(columns[2].strip()) cells[columns[1]] = { - 'uuid': columns[2], - 'amqp': columns[3], - 'db': columns[4]} + "uuid": columns[2], + "amqp": columns[3], + "db": columns[4], + } except ValueError: pass return cells def configure_charm(self, event: ops.framework.EventBase) -> None: + """Callback handler for nova operator configuration.""" if not self.peers.ready: return metadata_secret = self.get_shared_metadatasecret() @@ -441,12 +498,7 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): super().configure_charm(event) -class NovaXenaOperatorCharm(NovaOperatorCharm): - - openstack_release = 'xena' - - if __name__ == "__main__": # Note: use_juju_for_storage=True required per # https://github.com/canonical/operator/issues/506 - main(NovaXenaOperatorCharm, use_juju_for_storage=True) + main(NovaOperatorCharm, use_juju_for_storage=True) diff --git a/charms/nova-k8s/src/templates/parts/section-database b/charms/nova-k8s/src/templates/parts/section-database deleted file mode 100644 index eb52f65e..00000000 --- a/charms/nova-k8s/src/templates/parts/section-database +++ /dev/null @@ -1,7 +0,0 @@ -[database] -{% if database.connection -%} -connection = {{ database.connection }} -{% else -%} -connection = sqlite:////var/lib/cinder/cinder.db -{% endif -%} -connection_recycle_time = 200 diff --git a/charms/nova-k8s/src/templates/parts/section-federation b/charms/nova-k8s/src/templates/parts/section-federation deleted file mode 100644 index 65ee99ed..00000000 --- a/charms/nova-k8s/src/templates/parts/section-federation +++ /dev/null @@ -1,10 +0,0 @@ -{% if trusted_dashboards %} -[federation] -{% for dashboard_url in trusted_dashboards -%} -trusted_dashboard = {{ dashboard_url }} -{% endfor -%} -{% endif %} -{% for sp in fid_sps -%} -[{{ sp['protocol-name'] }}] -remote_id_attribute = {{ sp['remote-id-attribute'] }} -{% endfor -%} diff --git a/charms/nova-k8s/src/templates/parts/section-middleware b/charms/nova-k8s/src/templates/parts/section-middleware deleted file mode 100644 index e65f1d98..00000000 --- a/charms/nova-k8s/src/templates/parts/section-middleware +++ /dev/null @@ -1,6 +0,0 @@ -{% for section in sections -%} -[{{section}}] -{% for key, value in sections[section].items() -%} -{{ key }} = {{ value }} -{% endfor %} -{%- endfor %} diff --git a/charms/nova-k8s/src/templates/parts/section-signing b/charms/nova-k8s/src/templates/parts/section-signing deleted file mode 100644 index cb7d69ae..00000000 --- a/charms/nova-k8s/src/templates/parts/section-signing +++ /dev/null @@ -1,15 +0,0 @@ -{% if enable_signing -%} -[signing] -{% if certfile -%} -certfile = {{ certfile }} -{% endif -%} -{% if keyfile -%} -keyfile = {{ keyfile }} -{% endif -%} -{% if ca_certs -%} -ca_certs = {{ ca_certs }} -{% endif -%} -{% if ca_key -%} -ca_key = {{ ca_key }} -{% endif -%} -{% endif -%} \ No newline at end of file diff --git a/charms/nova-k8s/tests/unit/__init__.py b/charms/nova-k8s/tests/unit/__init__.py index e69de29b..ac683aca 100644 --- a/charms/nova-k8s/tests/unit/__init__.py +++ b/charms/nova-k8s/tests/unit/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2022 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. + +"""Unit tests for Nova operator.""" diff --git a/charms/nova-k8s/tests/unit/test_nova_charm.py b/charms/nova-k8s/tests/unit/test_nova_charm.py index b8ea2db8..ac7583f4 100644 --- a/charms/nova-k8s/tests/unit/test_nova_charm.py +++ b/charms/nova-k8s/tests/unit/test_nova_charm.py @@ -14,13 +14,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -import mock +"""Unit tests for Nova operator.""" -import charm +import mock import ops_sunbeam.test_utils as test_utils +import charm -class _NovaXenaOperatorCharm(charm.NovaXenaOperatorCharm): + +class _NovaTestOperatorCharm(charm.NovaOperatorCharm): + """Test Operator Charm for Nova Operator.""" def __init__(self, framework): self.seen_events = [] @@ -39,24 +42,28 @@ class _NovaXenaOperatorCharm(charm.NovaXenaOperatorCharm): class TestNovaOperatorCharm(test_utils.CharmTestCase): + """Unit tests for Nova Operator.""" PATCHES = [] @mock.patch( - 'charms.observability_libs.v0.kubernetes_service_patch.' - 'KubernetesServicePatch') + "charms.observability_libs.v0.kubernetes_service_patch." + "KubernetesServicePatch" + ) def setUp(self, mock_patch): + """Setup environment for unit test.""" super().setUp(charm, self.PATCHES) self.harness = test_utils.get_harness( - _NovaXenaOperatorCharm, - container_calls=self.container_calls) + _NovaTestOperatorCharm, container_calls=self.container_calls + ) # clean up events that were dynamically defined, # otherwise we get issues because they'll be redefined, # which is not allowed. from charms.data_platform_libs.v0.database_requires import ( - DatabaseEvents + DatabaseEvents, ) + for attr in ( "database_database_created", "database_endpoints_changed", @@ -77,11 +84,13 @@ class TestNovaOperatorCharm(test_utils.CharmTestCase): self.harness.begin() def test_pebble_ready_handler(self): + """Test pebble ready handler.""" self.assertEqual(self.harness.charm.seen_events, []) test_utils.set_all_pebbles_ready(self.harness) self.assertEqual(len(self.harness.charm.seen_events), 3) def test_all_relations(self): + """Test all integrations for operator.""" self.harness.set_leader() test_utils.set_all_pebbles_ready(self.harness) # this adds all the default/common relations @@ -95,25 +104,40 @@ class TestNovaOperatorCharm(test_utils.CharmTestCase): test_utils.add_db_relation_credentials(self.harness, rel_id) setup_cmds = [ - ['a2ensite', 'wsgi-nova-api'], - ['sudo', '-u', 'nova', 'nova-manage', 'api_db', 'sync'], + ["a2ensite", "wsgi-nova-api"], + ["sudo", "-u", "nova", "nova-manage", "api_db", "sync"], [ - 'sudo', '-u', 'nova', 'nova-manage', 'cell_v2', 'map_cell0', - '--database_connection', + "sudo", + "-u", + "nova", + "nova-manage", + "cell_v2", + "map_cell0", + "--database_connection", # values originate in test_utils.add_db_relation_credentials() - 'mysql+pymysql://foo:hardpassword@10.0.0.10/nova_cell0' + "mysql+pymysql://foo:hardpassword@10.0.0.10/nova_cell0", + ], + ["sudo", "-u", "nova", "nova-manage", "db", "sync"], + [ + "sudo", + "-u", + "nova", + "nova-manage", + "cell_v2", + "create_cell", + "--name", + "cell1", + "--verbose", ], - ['sudo', '-u', 'nova', 'nova-manage', 'db', 'sync'], - ['sudo', '-u', 'nova', 'nova-manage', 'cell_v2', 'create_cell', - '--name', 'cell1', '--verbose'], ] for cmd in setup_cmds: - self.assertIn(cmd, self.container_calls.execute['nova-api']) + self.assertIn(cmd, self.container_calls.execute["nova-api"]) config_files = [ - '/etc/apache2/sites-available/wsgi-nova-api.conf', - '/etc/nova/nova.conf'] + "/etc/apache2/sites-available/wsgi-nova-api.conf", + "/etc/nova/nova.conf", + ] for f in config_files: - self.check_file('nova-api', f) + self.check_file("nova-api", f) def add_db_relation(harness, name) -> str: diff --git a/charms/nova-k8s/tox.ini b/charms/nova-k8s/tox.ini index cfa25bf3..ea682f9a 100644 --- a/charms/nova-k8s/tox.ini +++ b/charms/nova-k8s/tox.ini @@ -15,6 +15,8 @@ minversion = 3.18.0 src_path = {toxinidir}/src/ tst_path = {toxinidir}/tests/ lib_path = {toxinidir}/lib/ +pyproject_toml = {toxinidir}/pyproject.toml +all_path = {[vars]src_path} {[vars]tst_path} [testenv] basepython = python3 @@ -33,6 +35,15 @@ allowlist_externals = deps = -r{toxinidir}/test-requirements.txt +[testenv:fmt] +description = Apply coding style standards to code +deps = + black + isort +commands = + isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox + black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} + [testenv:build] basepython = python3 deps = @@ -64,11 +75,6 @@ deps = {[testenv:py3]deps} basepython = python3.10 deps = {[testenv:py3]deps} -[testenv:pep8] -basepython = python3 -deps = {[testenv]deps} -commands = flake8 {posargs} {[vars]src_path} {[vars]tst_path} - [testenv:cover] basepython = python3 deps = {[testenv:py3]deps} @@ -83,6 +89,31 @@ commands = coverage xml -o cover/coverage.xml coverage report +[testenv:pep8] +description = Alias for lint +deps = {[testenv:lint]deps} +commands = {[testenv:lint]commands} + +[testenv:lint] +description = Check code against coding style standards +deps = + black + # flake8==4.0.1 # Pin version until https://github.com/csachs/pyproject-flake8/pull/14 is merged + flake8 + flake8-docstrings + flake8-copyright + flake8-builtins + pyproject-flake8 + pep8-naming + isort + codespell +commands = + codespell {[vars]all_path} + # pflake8 wrapper supports config from pyproject.toml + pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} + isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} + black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} + [testenv:func-noop] basepython = python3 commands =