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: Iabc3b12d8409d304ad3a021ebc293f0cd545e63d
This commit is contained in:
Hemanth Nakkina
2022-11-04 10:02:39 +05:30
parent d703a24e3b
commit b65c3e31e4
13 changed files with 183 additions and 117 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

@@ -17,11 +17,11 @@ 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 # 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 # 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 # git+https://opendev.org/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client
# Charmhelpers is only present as interface_ceph_client uses it. # Charmhelpers is only present as interface_ceph_client uses it.
git+https://github.com/juju/charm-helpers.git#egg=charmhelpers # 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.
"""OpenstackDashboard Operator Charm. """OpenstackDashboard Operator Charm.
This charm provide OpenstackDashboard services as part of an OpenStack This charm provide OpenstackDashboard services as part of an OpenStack
@@ -6,36 +20,38 @@ deployment
""" """
import logging import logging
from typing import List from typing import (
List,
)
import ops.framework import ops.framework
from ops.main import main
import ops_sunbeam.core as sunbeam_core
import ops_sunbeam.charm as sunbeam_charm import ops_sunbeam.charm as sunbeam_charm
import ops_sunbeam.container_handlers as sunbeam_chandlers import ops_sunbeam.container_handlers as sunbeam_chandlers
import ops_sunbeam.core as sunbeam_core
from ops.main import (
main,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class WSGIDashboardPebbleHandler(sunbeam_chandlers.WSGIPebbleHandler): class WSGIDashboardPebbleHandler(sunbeam_chandlers.WSGIPebbleHandler):
"""Dashboard Pebble Handler."""
def init_service(self, context: sunbeam_core.OPSCharmContexts) -> None: def init_service(self, context: sunbeam_core.OPSCharmContexts) -> None:
"""Enable and start WSGI service.""" """Enable and start WSGI service."""
container = self.charm.unit.get_container(self.container_name) container = self.charm.unit.get_container(self.container_name)
try: try:
process = container.exec( process = container.exec(
['a2dissite', '000-default'], ["a2dissite", "000-default"], timeout=5 * 60
timeout=5*60) )
out, warnings = process.wait_output() out, warnings = process.wait_output()
if warnings: if warnings:
for line in warnings.splitlines(): for line in warnings.splitlines():
logger.warning('a2dissite warn: %s', line.strip()) logger.warning("a2dissite warn: %s", line.strip())
logging.debug(f'Output from a2dissite: \n{out}') logging.debug(f"Output from a2dissite: \n{out}")
except ops.pebble.ExecError: except ops.pebble.ExecError:
logger.exception( logger.exception("Failed to disable default site in apache")
"Failed to disable default site in apache"
)
super().init_service(context) super().init_service(context)
@@ -45,26 +61,30 @@ class OpenstackDashboardOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
_state = ops.framework.StoredState() _state = ops.framework.StoredState()
service_name = "openstack-dashboard" service_name = "openstack-dashboard"
wsgi_admin_script = ( wsgi_admin_script = (
"/usr/share/openstack-dashboard/openstack_dashboard/wsgi/django.wsgi") "/usr/share/openstack-dashboard/openstack_dashboard/wsgi/django.wsgi"
)
wsgi_public_script = ( wsgi_public_script = (
"/usr/share/openstack-dashboard/openstack_dashboard/wsgi/django.wsgi") "/usr/share/openstack-dashboard/openstack_dashboard/wsgi/django.wsgi"
)
db_sync_cmds = [ db_sync_cmds = [
[ [
'python3', "python3",
'/usr/share/openstack-dashboard/manage.py', "/usr/share/openstack-dashboard/manage.py",
'migrate', "migrate",
'--noinput'] "--noinput",
]
] ]
mandatory_relations = { mandatory_relations = {
'database', "database",
'ingress-public', "ingress-public",
'cloud-credentials', "cloud-credentials",
} }
@property @property
def default_public_ingress_port(self): def default_public_ingress_port(self):
"""Default public ingress port."""
return 80 return 80
@property @property
@@ -80,23 +100,26 @@ class OpenstackDashboardOperatorCharm(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 'horizon' return "horizon"
@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 'horizon' return "horizon"
@property @property
def service_endpoints(self): def service_endpoints(self):
"""Endpoints for horizon."""
return [ return [
{ {
'service_name': 'openstack-dashboard', "service_name": "openstack-dashboard",
'type': 'openstack-dashboard', "type": "openstack-dashboard",
'description': "OpenStack OpenstackDashboard API", "description": "OpenStack OpenstackDashboard API",
'internal_url': f'{self.internal_url}', "internal_url": f"{self.internal_url}",
'public_url': f'{self.public_url}', "public_url": f"{self.public_url}",
'admin_url': f'{self.admin_url}'}] "admin_url": f"{self.admin_url}",
}
]
def get_pebble_handlers(self) -> List[sunbeam_chandlers.PebbleHandler]: def get_pebble_handlers(self) -> List[sunbeam_chandlers.PebbleHandler]:
"""Pebble handlers for the service.""" """Pebble handlers for the service."""
@@ -107,7 +130,6 @@ class OpenstackDashboardOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
self.service_name, self.service_name,
self.container_configs, self.container_configs,
self.template_dir, self.template_dir,
self.openstack_release,
self.configure_charm, self.configure_charm,
f"wsgi-{self.service_name}", f"wsgi-{self.service_name}",
) )
@@ -117,16 +139,10 @@ class OpenstackDashboardOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
"""Configure charm services.""" """Configure charm services."""
super().configure_charm(event) super().configure_charm(event)
if self.bootstrapped(): if self.bootstrapped():
self.unit.status = ops.model.ActiveStatus( self.unit.status = ops.model.ActiveStatus(self.ingress_public.url)
self.ingress_public.url)
class OpenstackDashboardXenaOperatorCharm(OpenstackDashboardOperatorCharm):
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(OpenstackDashboardXenaOperatorCharm, use_juju_for_storage=True) main(OpenstackDashboardOperatorCharm, use_juju_for_storage=True)

View File

@@ -498,7 +498,7 @@ ENFORCE_PASSWORD_CHECK = True
# Path to directory containing policy.json files # Path to directory containing policy.json files
#POLICY_FILES_PATH = os.path.join(ROOT_PATH, "conf") #POLICY_FILES_PATH = os.path.join(ROOT_PATH, "conf")
# Policies are overriden and all policies are here rather than in package conf # Policies are overridden and all policies are here rather than in package conf
# POLICY_FILES_PATH = '/etc/openstack-dashboard/policy.d/' # POLICY_FILES_PATH = '/etc/openstack-dashboard/policy.d/'
# These are matched from the defaults + any in the overrides # These are matched from the defaults + any in the overrides

View File

@@ -1,13 +0,0 @@
{% if identity_service.internal_auth_url -%}
www_authenticate_uri = {{ identity_service.internal_auth_url }}
auth_url = {{ identity_service.internal_auth_url }}
{% elif identity_service.internal_host -%}
www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
{% endif -%}
auth_type = password
project_domain_name = {{ identity_service.service_domain_name }}
user_domain_name = {{ identity_service.service_domain_name }}
project_name = {{ identity_service.service_project_name }}
username = {{ identity_service.service_user_name }}
password = {{ identity_service.service_password }}

View File

@@ -1,3 +0,0 @@
[database]
{% include "parts/database-connection" %}
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,2 +0,0 @@
[keystone_authtoken]
{% include "parts/identity-data" %}

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 Horizon operator."""

View File

@@ -14,14 +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 Horizon operator."""
import charm import mock
import ops_sunbeam.test_utils as test_utils import ops_sunbeam.test_utils as test_utils
import charm
class _DashboardXenaOperatorCharm(
charm.OpenstackDashboardXenaOperatorCharm): class _DashboardOperatorCharm(charm.OpenstackDashboardOperatorCharm):
"""Test Operator Charm for Horizon Operator."""
def __init__(self, framework): def __init__(self, framework):
self.seen_events = [] self.seen_events = []
@@ -36,28 +38,32 @@ class _DashboardXenaOperatorCharm(
@property @property
def public_ingress_address(self): def public_ingress_address(self):
return 'dashboard.juju' return "dashboard.juju"
class TestDashboardOperatorCharm(test_utils.CharmTestCase): class TestDashboardOperatorCharm(test_utils.CharmTestCase):
"""Unit tests for Horizon 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(
_DashboardXenaOperatorCharm, _DashboardOperatorCharm, 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",
@@ -72,27 +78,35 @@ class TestDashboardOperatorCharm(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(self.harness.charm.seen_events, ['PebbleReadyEvent']) self.assertEqual(self.harness.charm.seen_events, ["PebbleReadyEvent"])
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)
test_utils.add_all_relations(self.harness) test_utils.add_all_relations(self.harness)
test_utils.add_complete_ingress_relation(self.harness) test_utils.add_complete_ingress_relation(self.harness)
setup_cmds = [ setup_cmds = [
['a2dissite', '000-default'], ["a2dissite", "000-default"],
['a2ensite', 'wsgi-openstack-dashboard'], ["a2ensite", "wsgi-openstack-dashboard"],
['python3', '/usr/share/openstack-dashboard/manage.py', 'migrate', [
'--noinput']] "python3",
"/usr/share/openstack-dashboard/manage.py",
"migrate",
"--noinput",
],
]
for cmd in setup_cmds: for cmd in setup_cmds:
self.assertIn( self.assertIn(
cmd, cmd, self.container_calls.execute["openstack-dashboard"]
self.container_calls.execute['openstack-dashboard']) )
self.check_file( self.check_file(
'openstack-dashboard', "openstack-dashboard",
'/etc/apache2/sites-available/wsgi-openstack-dashboard.conf') "/etc/apache2/sites-available/wsgi-openstack-dashboard.conf",
)
self.check_file( self.check_file(
'openstack-dashboard', "openstack-dashboard", "/etc/openstack-dashboard/local_settings.py"
'/etc/openstack-dashboard/local_settings.py') )

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}
[coverage:run] [coverage:run]
branch = True branch = True
concurrency = multiprocessing concurrency = multiprocessing