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
This commit is contained in:
James Page 2022-11-03 15:17:03 +01:00
parent a0a7785924
commit 49173f55dd
10 changed files with 296 additions and 181 deletions

View File

@ -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"

View File

@ -16,12 +16,4 @@ lightkube
lightkube-models lightkube-models
ops ops
git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam 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 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

View File

@ -1,4 +1,18 @@
#!/usr/bin/env python3 #!/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. """Nova Operator Charm.
This charm provide Nova services as part of an OpenStack deployment 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 logging
import uuid import uuid
from typing import Callable, List, Mapping from typing import (
Callable,
import ops.framework List,
from ops.main import main Mapping,
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
import charms.sunbeam_nova_compute_operator.v0.cloud_compute as cloud_compute 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__) logger = logging.getLogger(__name__)
@ -31,23 +51,24 @@ class WSGINovaMetadataConfigContext(sunbeam_ctxts.ConfigContext):
def context(self) -> dict: def context(self) -> dict:
"""WSGI configuration options.""" """WSGI configuration options."""
log_svc_name = self.charm.service_name.replace('-', '_') log_svc_name = self.charm.service_name.replace("-", "_")
return { return {
"name": self.charm.service_name, "name": self.charm.service_name,
"public_port": 8775, "public_port": 8775,
"user": self.charm.service_user, "user": self.charm.service_user,
"group": self.charm.service_group, "group": self.charm.service_group,
"wsgi_admin_script": '/usr/bin/nova-metadata-wsgi', "wsgi_admin_script": "/usr/bin/nova-metadata-wsgi",
"wsgi_public_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", "error_log": f"/var/log/apache2/{log_svc_name}_error.log",
"custom_log": f"/var/log/apache2/{log_svc_name}_access.log", "custom_log": f"/var/log/apache2/{log_svc_name}_access.log",
} }
class NovaSchedulerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): class NovaSchedulerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
"""Pebble handler for Nova scheduler."""
def get_layer(self) -> dict: def get_layer(self) -> dict:
"""Nova Scheduler service """Nova Scheduler service layer.
:returns: pebble layer configuration for scheduler service :returns: pebble layer configuration for scheduler service
:rtype: dict :rtype: dict
@ -60,9 +81,9 @@ class NovaSchedulerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
"override": "replace", "override": "replace",
"summary": "Nova Scheduler", "summary": "Nova Scheduler",
"command": "nova-scheduler", "command": "nova-scheduler",
"startup": "enabled" "startup": "enabled",
} }
} },
} }
def get_healthcheck_layer(self) -> dict: def get_healthcheck_layer(self) -> dict:
@ -76,25 +97,27 @@ class NovaSchedulerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
"online": { "online": {
"override": "replace", "override": "replace",
"level": "ready", "level": "ready",
"exec": { "exec": {"command": "service nova-scheduler status"},
"command": "service nova-scheduler status"
}
}, },
} }
} }
def default_container_configs(self): def default_container_configs(
self,
) -> List[sunbeam_core.ContainerConfigFile]:
"""Container configurations for handler."""
return [ return [
sunbeam_core.ContainerConfigFile( sunbeam_core.ContainerConfigFile(
'/etc/nova/nova.conf', "/etc/nova/nova.conf", "nova", "nova"
'nova', )
'nova')] ]
class NovaConductorPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): class NovaConductorPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
"""Pebble handler for Nova Conductor container."""
def get_layer(self): def get_layer(self):
"""Nova Conductor service """Nova Conductor service.
:returns: pebble service layer configuration for conductor service :returns: pebble service layer configuration for conductor service
:rtype: dict :rtype: dict
@ -107,9 +130,9 @@ class NovaConductorPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
"override": "replace", "override": "replace",
"summary": "Nova Conductor", "summary": "Nova Conductor",
"command": "nova-conductor", "command": "nova-conductor",
"startup": "enabled" "startup": "enabled",
} }
} },
} }
def get_healthcheck_layer(self) -> dict: def get_healthcheck_layer(self) -> dict:
@ -122,19 +145,20 @@ class NovaConductorPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
"online": { "online": {
"override": "replace", "override": "replace",
"level": "ready", "level": "ready",
"exec": { "exec": {"command": "service nova-conductor status"},
"command": "service nova-conductor status"
}
}, },
} }
} }
def default_container_configs(self): def default_container_configs(
self,
) -> List[sunbeam_core.ContainerConfigFile]:
"""Container configurations for handler."""
return [ return [
sunbeam_core.ContainerConfigFile( sunbeam_core.ContainerConfigFile(
'/etc/nova/nova.conf', "/etc/nova/nova.conf", "nova", "nova"
'nova', )
'nova')] ]
class CloudComputeRequiresHandler(sunbeam_rhandlers.RelationHandler): class CloudComputeRequiresHandler(sunbeam_rhandlers.RelationHandler):
@ -148,7 +172,9 @@ class CloudComputeRequiresHandler(sunbeam_rhandlers.RelationHandler):
callback_f: Callable, callback_f: Callable,
mandatory: bool = False, 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 events from the relation and invokes the provided callbacks based on
the event raised. the event raised.
@ -175,11 +201,11 @@ class CloudComputeRequiresHandler(sunbeam_rhandlers.RelationHandler):
) )
self.framework.observe( self.framework.observe(
compute_service.on.compute_nodes_connected, compute_service.on.compute_nodes_connected,
self._compute_nodes_connected self._compute_nodes_connected,
) )
self.framework.observe( self.framework.observe(
compute_service.on.compute_nodes_ready, compute_service.on.compute_nodes_ready,
self._compute_nodes_connected self._compute_nodes_connected,
) )
return compute_service return compute_service
@ -191,6 +217,7 @@ class CloudComputeRequiresHandler(sunbeam_rhandlers.RelationHandler):
@property @property
def ready(self) -> bool: def ready(self) -> bool:
"""Interface ready for use."""
return True return True
@ -199,20 +226,21 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
_state = ops.framework.StoredState() _state = ops.framework.StoredState()
service_name = "nova-api" service_name = "nova-api"
wsgi_admin_script = '/usr/bin/nova-api-wsgi' wsgi_admin_script = "/usr/bin/nova-api-wsgi"
wsgi_public_script = '/usr/bin/nova-api-wsgi' wsgi_public_script = "/usr/bin/nova-api-wsgi"
shared_metadata_secret_key = 'shared-metadata-secret' shared_metadata_secret_key = "shared-metadata-secret"
mandatory_relations = { mandatory_relations = {
'database', "database",
'api-database', "api-database",
'cell-database', "cell-database",
'amqp', "amqp",
'identity-service', "identity-service",
'ingress-public', "ingress-public",
} }
@property @property
def db_sync_cmds(self) -> List[List[str]]: def db_sync_cmds(self) -> List[List[str]]:
"""DB sync commands for Nova operator."""
# we must provide the database connection for the cell database, # we must provide the database connection for the cell database,
# because the database credentials are different to the main database. # because the database credentials are different to the main database.
# If we don't provide them: # 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 # https://docs.openstack.org/nova/yoga/admin/cells.html#configuring-a-new-deployment
cell_database = self.dbs["cell-database"].context()["connection"] cell_database = self.dbs["cell-database"].context()["connection"]
return [ 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', "sudo",
'--database_connection', cell_database "-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 @property
@ -240,26 +283,30 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
@property @property
def service_user(self) -> str: def service_user(self) -> str:
"""Service user file and directory ownership.""" """Service user file and directory ownership."""
return 'nova' return "nova"
@property @property
def service_group(self) -> str: def service_group(self) -> str:
"""Service group file and directory ownership.""" """Service group file and directory ownership."""
return 'nova' return "nova"
@property @property
def service_endpoints(self): def service_endpoints(self):
"""Service endpoints for Nova."""
return [ return [
{ {
'service_name': 'nova', "service_name": "nova",
'type': 'compute', "type": "compute",
'description': "OpenStack Compute", "description": "OpenStack Compute",
'internal_url': f'{self.internal_url}/v2.1', "internal_url": f"{self.internal_url}/v2.1",
'public_url': f'{self.public_url}/v2.1', "public_url": f"{self.public_url}/v2.1",
'admin_url': f'{self.admin_url}/v2.1'}] "admin_url": f"{self.admin_url}/v2.1",
}
]
@property @property
def default_public_ingress_port(self): def default_public_ingress_port(self):
"""Default port for service ingress."""
return 8774 return 8774
@property @property
@ -275,41 +322,43 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
"cell-database": "nova_cell0", "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 = super().get_pebble_handlers()
pebble_handlers.extend([ pebble_handlers.extend(
NovaSchedulerPebbleHandler( [
self, NovaSchedulerPebbleHandler(
NOVA_SCHEDULER_CONTAINER, self,
'nova-scheduler', NOVA_SCHEDULER_CONTAINER,
[], "nova-scheduler",
self.template_dir, [],
self.openstack_release, self.template_dir,
self.configure_charm), self.configure_charm,
NovaConductorPebbleHandler( ),
self, NovaConductorPebbleHandler(
NOVA_CONDUCTOR_CONTAINER, self,
'nova-conductor', NOVA_CONDUCTOR_CONTAINER,
[], "nova-conductor",
self.template_dir, [],
self.openstack_release, self.template_dir,
self.configure_charm)]) self.configure_charm,
),
]
)
return pebble_handlers return pebble_handlers
def get_relation_handlers( def get_relation_handlers(
self, handlers: List[sunbeam_rhandlers.RelationHandler] = None self, handlers: List[sunbeam_rhandlers.RelationHandler] = None
) -> List[sunbeam_rhandlers.RelationHandler]: ) -> List[sunbeam_rhandlers.RelationHandler]:
""" """Relation handlers for operator."""
:param handlers:
:return:
"""
handlers = super().get_relation_handlers(handlers or []) handlers = super().get_relation_handlers(handlers or [])
if self.can_add_handler("cloud-compute", handlers): if self.can_add_handler("cloud-compute", handlers):
self.compute_nodes = CloudComputeRequiresHandler( self.compute_nodes = CloudComputeRequiresHandler(
self, self,
'cloud-compute', "cloud-compute",
self.model.config['region'], self.model.config["region"],
self.register_compute_nodes, self.register_compute_nodes,
) )
handlers.append(self.compute_nodes) handlers.append(self.compute_nodes)
@ -322,7 +371,8 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
_cadapters.extend( _cadapters.extend(
[ [
WSGINovaMetadataConfigContext( WSGINovaMetadataConfigContext(
self, 'wsgi_nova_metadata', self,
"wsgi_nova_metadata",
) )
] ]
) )
@ -334,8 +384,7 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
def set_shared_metadatasecret(self): def set_shared_metadatasecret(self):
"""Store the shared metadata secret.""" """Store the shared metadata secret."""
self.leader_set( self.leader_set({self.shared_metadata_secret_key: str(uuid.uuid1())})
{self.shared_metadata_secret_key: str(uuid.uuid1())})
def register_compute_nodes(self, event: ops.framework.EventBase) -> None: def register_compute_nodes(self, event: ops.framework.EventBase) -> None:
"""Register compute nodes when the event is received. """Register compute nodes when the event is received.
@ -363,27 +412,33 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
# return # return
self.compute_nodes.interface.set_controller_info( self.compute_nodes.interface.set_controller_info(
region=self.model.config['region'], region=self.model.config["region"],
cross_az_attach=False, cross_az_attach=False,
) )
try: try:
logger.debug('Discovering hosts for cell1') logger.debug("Discovering hosts for cell1")
cell1_uuid = self.get_cell_uuid('cell1') cell1_uuid = self.get_cell_uuid("cell1")
cmd = ['nova-manage', 'cell_v2', 'discover_hosts', '--cell_uuid', cmd = [
cell1_uuid, '--verbose'] "nova-manage",
"cell_v2",
"discover_hosts",
"--cell_uuid",
cell1_uuid,
"--verbose",
]
handler.execute(cmd, exception_on_error=True) handler.execute(cmd, exception_on_error=True)
except ExecError: except ExecError:
logger.exception('Failed to discover hosts for cell1') logger.exception("Failed to discover hosts for cell1")
raise raise
def get_cell_uuid(self, cell, fatal=True): 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' :param cell: string cell name i.e. 'cell1'
:returns: string cell uuid :returns: string cell uuid
""" """
logger.debug(f'listing cells for {cell}') logger.debug(f"listing cells for {cell}")
cells = self.get_cells() cells = self.get_cells()
cell_info = cells.get(cell) cell_info = cells.get(cell)
if not cell_info: if not cell_info:
@ -391,7 +446,7 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
raise Exception(f"Cell {cell} not found") raise Exception(f"Cell {cell} not found")
return None return None
return cell_info['uuid'] return cell_info["uuid"]
def get_cells(self): def get_cells(self):
"""Returns the cells configured in the environment. """Returns the cells configured in the environment.
@ -401,31 +456,33 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
""" """
logger.info("Getting details of cells") logger.info("Getting details of cells")
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) handler = self.get_named_pebble_handler(NOVA_CONDUCTOR_CONTAINER)
try: try:
out = handler.execute(cmd, exception_on_error=True) out = handler.execute(cmd, exception_on_error=True)
except ExecError: except ExecError:
logger.exception('list_cells failed') logger.exception("list_cells failed")
raise raise
for line in out.split('\n'): for line in out.split("\n"):
columns = line.split('|') columns = line.split("|")
if len(columns) < 2: if len(columns) < 2:
continue continue
columns = [c.strip() for c in columns] columns = [c.strip() for c in columns]
try: try:
uuid.UUID(columns[2].strip()) uuid.UUID(columns[2].strip())
cells[columns[1]] = { cells[columns[1]] = {
'uuid': columns[2], "uuid": columns[2],
'amqp': columns[3], "amqp": columns[3],
'db': columns[4]} "db": columns[4],
}
except ValueError: except ValueError:
pass pass
return cells return cells
def configure_charm(self, event: ops.framework.EventBase) -> None: def configure_charm(self, event: ops.framework.EventBase) -> None:
"""Callback handler for nova operator configuration."""
if not self.peers.ready: if not self.peers.ready:
return return
metadata_secret = self.get_shared_metadatasecret() metadata_secret = self.get_shared_metadatasecret()
@ -441,12 +498,7 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
super().configure_charm(event) super().configure_charm(event)
class NovaXenaOperatorCharm(NovaOperatorCharm):
openstack_release = 'xena'
if __name__ == "__main__": if __name__ == "__main__":
# Note: use_juju_for_storage=True required per # Note: use_juju_for_storage=True required per
# https://github.com/canonical/operator/issues/506 # https://github.com/canonical/operator/issues/506
main(NovaXenaOperatorCharm, use_juju_for_storage=True) main(NovaOperatorCharm, use_juju_for_storage=True)

View File

@ -1,7 +0,0 @@
[database]
{% if database.connection -%}
connection = {{ database.connection }}
{% else -%}
connection = sqlite:////var/lib/cinder/cinder.db
{% endif -%}
connection_recycle_time = 200

View File

@ -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 -%}

View File

@ -1,6 +0,0 @@
{% for section in sections -%}
[{{section}}]
{% for key, value in sections[section].items() -%}
{{ key }} = {{ value }}
{% endfor %}
{%- endfor %}

View File

@ -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 -%}

View File

@ -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."""

View File

@ -14,13 +14,16 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import mock """Unit tests for Nova operator."""
import charm import mock
import ops_sunbeam.test_utils as test_utils 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): def __init__(self, framework):
self.seen_events = [] self.seen_events = []
@ -39,24 +42,28 @@ class _NovaXenaOperatorCharm(charm.NovaXenaOperatorCharm):
class TestNovaOperatorCharm(test_utils.CharmTestCase): class TestNovaOperatorCharm(test_utils.CharmTestCase):
"""Unit tests for Nova Operator."""
PATCHES = [] PATCHES = []
@mock.patch( @mock.patch(
'charms.observability_libs.v0.kubernetes_service_patch.' "charms.observability_libs.v0.kubernetes_service_patch."
'KubernetesServicePatch') "KubernetesServicePatch"
)
def setUp(self, mock_patch): def setUp(self, mock_patch):
"""Setup environment for unit test."""
super().setUp(charm, self.PATCHES) super().setUp(charm, self.PATCHES)
self.harness = test_utils.get_harness( self.harness = test_utils.get_harness(
_NovaXenaOperatorCharm, _NovaTestOperatorCharm, container_calls=self.container_calls
container_calls=self.container_calls) )
# clean up events that were dynamically defined, # clean up events that were dynamically defined,
# otherwise we get issues because they'll be redefined, # otherwise we get issues because they'll be redefined,
# which is not allowed. # which is not allowed.
from charms.data_platform_libs.v0.database_requires import ( from charms.data_platform_libs.v0.database_requires import (
DatabaseEvents DatabaseEvents,
) )
for attr in ( for attr in (
"database_database_created", "database_database_created",
"database_endpoints_changed", "database_endpoints_changed",
@ -77,11 +84,13 @@ class TestNovaOperatorCharm(test_utils.CharmTestCase):
self.harness.begin() self.harness.begin()
def test_pebble_ready_handler(self): def test_pebble_ready_handler(self):
"""Test pebble ready handler."""
self.assertEqual(self.harness.charm.seen_events, []) self.assertEqual(self.harness.charm.seen_events, [])
test_utils.set_all_pebbles_ready(self.harness) test_utils.set_all_pebbles_ready(self.harness)
self.assertEqual(len(self.harness.charm.seen_events), 3) self.assertEqual(len(self.harness.charm.seen_events), 3)
def test_all_relations(self): def test_all_relations(self):
"""Test all integrations for operator."""
self.harness.set_leader() self.harness.set_leader()
test_utils.set_all_pebbles_ready(self.harness) test_utils.set_all_pebbles_ready(self.harness)
# this adds all the default/common relations # 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) test_utils.add_db_relation_credentials(self.harness, rel_id)
setup_cmds = [ setup_cmds = [
['a2ensite', 'wsgi-nova-api'], ["a2ensite", "wsgi-nova-api"],
['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', "sudo",
'--database_connection', "-u",
"nova",
"nova-manage",
"cell_v2",
"map_cell0",
"--database_connection",
# values originate in test_utils.add_db_relation_credentials() # 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: 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 = [ config_files = [
'/etc/apache2/sites-available/wsgi-nova-api.conf', "/etc/apache2/sites-available/wsgi-nova-api.conf",
'/etc/nova/nova.conf'] "/etc/nova/nova.conf",
]
for f in config_files: for f in config_files:
self.check_file('nova-api', f) self.check_file("nova-api", f)
def add_db_relation(harness, name) -> str: def add_db_relation(harness, name) -> str:

View File

@ -15,6 +15,8 @@ minversion = 3.18.0
src_path = {toxinidir}/src/ src_path = {toxinidir}/src/
tst_path = {toxinidir}/tests/ tst_path = {toxinidir}/tests/
lib_path = {toxinidir}/lib/ lib_path = {toxinidir}/lib/
pyproject_toml = {toxinidir}/pyproject.toml
all_path = {[vars]src_path} {[vars]tst_path}
[testenv] [testenv]
basepython = python3 basepython = python3
@ -33,6 +35,15 @@ allowlist_externals =
deps = deps =
-r{toxinidir}/test-requirements.txt -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] [testenv:build]
basepython = python3 basepython = python3
deps = deps =
@ -64,11 +75,6 @@ deps = {[testenv:py3]deps}
basepython = python3.10 basepython = python3.10
deps = {[testenv:py3]deps} deps = {[testenv:py3]deps}
[testenv:pep8]
basepython = python3
deps = {[testenv]deps}
commands = flake8 {posargs} {[vars]src_path} {[vars]tst_path}
[testenv:cover] [testenv:cover]
basepython = python3 basepython = python3
deps = {[testenv:py3]deps} deps = {[testenv:py3]deps}
@ -83,6 +89,31 @@ commands =
coverage xml -o cover/coverage.xml coverage xml -o cover/coverage.xml
coverage report 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] [testenv:func-noop]
basepython = python3 basepython = python3
commands = commands =