Initial cut of ceph-dashboard charm
This commit is contained in:
commit
0d463e0de2
9
.flake8
Normal file
9
.flake8
Normal file
@ -0,0 +1,9 @@
|
||||
[flake8]
|
||||
max-line-length = 99
|
||||
select: E,W,F,C,N
|
||||
exclude:
|
||||
venv
|
||||
.git
|
||||
build
|
||||
dist
|
||||
*.egg_info
|
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
.tox
|
||||
**/*.swp
|
||||
__pycache__
|
||||
.stestr/
|
||||
lib/*
|
||||
!lib/README.txt
|
||||
build
|
||||
*.charm
|
3
.jujuignore
Normal file
3
.jujuignore
Normal file
@ -0,0 +1,3 @@
|
||||
/venv
|
||||
*.py[cod]
|
||||
*.charm
|
3
.stestr.conf
Normal file
3
.stestr.conf
Normal file
@ -0,0 +1,3 @@
|
||||
[DEFAULT]
|
||||
test_path=./unit_tests
|
||||
top_dir=./
|
202
LICENSE
Normal file
202
LICENSE
Normal file
@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
27
README.md
Normal file
27
README.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Overview
|
||||
|
||||
The ceph-dashboard configures the [Ceph Dashboard][ceph-dashboard-upstream].
|
||||
The charm is intended to be used in conjunction with the
|
||||
[ceph-mon][ceph-mon-charm] charm.
|
||||
|
||||
# Usage
|
||||
|
||||
## Configuration
|
||||
|
||||
See file `config.yaml` for the full list of options, along with their
|
||||
descriptions and default values.
|
||||
|
||||
## Deployment
|
||||
|
||||
We are assuming a pre-existing Ceph cluster.
|
||||
|
||||
Deploy the ceph-dashboard as a subordinate to the ceph-mon charm.
|
||||
|
||||
juju deploy ceph-dashboard
|
||||
juju relate ceph-dashboard ceph-mon
|
||||
|
||||
|
||||
<!-- LINKS -->
|
||||
|
||||
[ceph-dashboard]: https://docs.ceph.com/en/latest/mgr/dashboard/
|
||||
[ceph-mon-charm]: https://jaas.ai/ceph-mon
|
14
actions.yaml
Normal file
14
actions.yaml
Normal file
@ -0,0 +1,14 @@
|
||||
# Copyright 2021 Canonical
|
||||
# See LICENSE file for licensing details.
|
||||
|
||||
add-user:
|
||||
description: add a dashboard user
|
||||
params:
|
||||
username:
|
||||
description: Name of user to create
|
||||
type: string
|
||||
default: ""
|
||||
role:
|
||||
description: Role to give user
|
||||
type: string
|
||||
default: ""
|
10
config.yaml
Normal file
10
config.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
# Copyright 2021 Canonical
|
||||
# See LICENSE file for licensing details.
|
||||
|
||||
options:
|
||||
public-hostname:
|
||||
type: string
|
||||
default:
|
||||
description: |
|
||||
The hostname or address of the public endpoints created for the
|
||||
dashboard
|
27
metadata.yaml
Normal file
27
metadata.yaml
Normal file
@ -0,0 +1,27 @@
|
||||
# Copyright 2021 Canonical
|
||||
# See LICENSE file for licensing details.
|
||||
name: ceph-dashboard
|
||||
display-name: Ceph Dashboard
|
||||
maintainer: OpenStack Charmers <openstack-discuss@lists.openstack.org>
|
||||
summary: Enable dashboard for Ceph
|
||||
description: |
|
||||
Enable the ceph dashboard on the ceph mon units
|
||||
tags:
|
||||
- openstack
|
||||
- storage
|
||||
- backup
|
||||
extra-bindings:
|
||||
public:
|
||||
subordinate: true
|
||||
series:
|
||||
- focal
|
||||
- groovy
|
||||
requires:
|
||||
dashboard:
|
||||
interface: ceph-dashboard
|
||||
scope: container
|
||||
certificates:
|
||||
interface: tls-certificates
|
||||
loadbalancer:
|
||||
interface: api-endpoints
|
||||
|
3
requirements-dev.txt
Normal file
3
requirements-dev.txt
Normal file
@ -0,0 +1,3 @@
|
||||
-r requirements.txt
|
||||
coverage
|
||||
flake8
|
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@ -0,0 +1,5 @@
|
||||
ops >= 1.2.0
|
||||
git+https://github.com/openstack/charms.ceph#egg=charms_ceph
|
||||
git+https://opendev.org/openstack/charm-ops-openstack#egg=ops_openstack
|
||||
#git+https://opendev.org/openstack/charm-ops-interface-tls-certificates#egg=interface_tls_certificates
|
||||
git+https://github.com/gnuoy/ops-interface-tls-certificates@no-exception-for-inflight-request#egg=interface_tls_certificates
|
192
src/charm.py
Executable file
192
src/charm.py
Executable file
@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright 2021 Canonical
|
||||
# See LICENSE file for licensing details.
|
||||
#
|
||||
# Learn more at: https://juju.is/docs/sdk
|
||||
|
||||
"""Charm for the Ceph Dashboard."""
|
||||
|
||||
import logging
|
||||
import tempfile
|
||||
|
||||
from ops.framework import StoredState
|
||||
from ops.main import main
|
||||
from ops.model import ActiveStatus, BlockedStatus, StatusBase
|
||||
from ops.charm import ActionEvent
|
||||
import interface_tls_certificates.ca_client as ca_client
|
||||
import re
|
||||
import secrets
|
||||
import socket
|
||||
import string
|
||||
import subprocess
|
||||
import ops_openstack.plugins.classes
|
||||
import interface_dashboard
|
||||
import interface_api_endpoints
|
||||
import cryptography.hazmat.primitives.serialization as serialization
|
||||
import charms_ceph.utils as ceph_utils
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CephDashboardCharm(ops_openstack.core.OSBaseCharm):
|
||||
"""Ceph Dashboard charm."""
|
||||
|
||||
_stored = StoredState()
|
||||
PACKAGES = ['ceph-mgr-dashboard']
|
||||
CEPH_CONFIG_PATH = Path('/etc/ceph')
|
||||
TLS_KEY_PATH = CEPH_CONFIG_PATH / 'ceph-dashboard.key'
|
||||
TLS_PUB_KEY_PATH = CEPH_CONFIG_PATH / 'ceph-dashboard-pub.key'
|
||||
TLS_CERT_PATH = CEPH_CONFIG_PATH / 'ceph-dashboard.crt'
|
||||
TLS_KEY_AND_CERT_PATH = CEPH_CONFIG_PATH / 'ceph-dashboard.pem'
|
||||
TLS_CA_CERT_PATH = Path(
|
||||
'/usr/local/share/ca-certificates/vault_ca_cert_dashboard.crt')
|
||||
TLS_PORT = 8443
|
||||
|
||||
def __init__(self, *args) -> None:
|
||||
"""Setup adapters and observers."""
|
||||
super().__init__(*args)
|
||||
super().register_status_check(self.check_dashboard)
|
||||
self.mon = interface_dashboard.CephDashboardRequires(
|
||||
self,
|
||||
'dashboard')
|
||||
self.ca_client = ca_client.CAClient(
|
||||
self,
|
||||
'certificates')
|
||||
self.framework.observe(
|
||||
self.mon.on.mon_ready,
|
||||
self._configure_dashboard)
|
||||
self.framework.observe(
|
||||
self.ca_client.on.ca_available,
|
||||
self._on_ca_available)
|
||||
self.framework.observe(
|
||||
self.ca_client.on.tls_server_config_ready,
|
||||
self._on_tls_server_config_ready)
|
||||
self.framework.observe(self.on.add_user_action, self._add_user_action)
|
||||
self.ingress = interface_api_endpoints.APIEndpointsRequires(
|
||||
self,
|
||||
'loadbalancer',
|
||||
{
|
||||
'endpoints': [{
|
||||
'service-type': 'ceph-dashboard',
|
||||
'frontend-port': self.TLS_PORT,
|
||||
'backend-port': self.TLS_PORT,
|
||||
'backend-ip': self._get_bind_ip(),
|
||||
'check-type': 'httpd'}]})
|
||||
self._stored.set_default(is_started=False)
|
||||
|
||||
def _on_ca_available(self, _) -> None:
|
||||
"""Request TLS certificates."""
|
||||
addresses = set()
|
||||
for binding_name in ['public']:
|
||||
binding = self.model.get_binding(binding_name)
|
||||
addresses.add(binding.network.ingress_address)
|
||||
addresses.add(binding.network.bind_address)
|
||||
sans = [str(s) for s in addresses]
|
||||
sans.append(socket.gethostname())
|
||||
if self.config.get('public-hostname'):
|
||||
sans.append(self.config.get('public-hostname'))
|
||||
self.ca_client.request_server_certificate(socket.getfqdn(), sans)
|
||||
|
||||
def check_dashboard(self) -> StatusBase:
|
||||
"""Check status of dashboard"""
|
||||
self._stored.is_started = ceph_utils.is_dashboard_enabled()
|
||||
if self._stored.is_started:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
result = sock.connect_ex((self._get_bind_ip(), self.TLS_PORT))
|
||||
if result == 0:
|
||||
return ActiveStatus()
|
||||
else:
|
||||
return BlockedStatus(
|
||||
'Dashboard not responding')
|
||||
else:
|
||||
return BlockedStatus(
|
||||
'Dashboard is not enabled')
|
||||
return ActiveStatus()
|
||||
|
||||
def kick_dashboard(self) -> None:
|
||||
"""Disable and re-enable dashboard"""
|
||||
ceph_utils.mgr_disable_dashboard()
|
||||
ceph_utils.mgr_enable_dashboard()
|
||||
|
||||
def _configure_dashboard(self, _) -> None:
|
||||
"""Configure dashboard"""
|
||||
if self.unit.is_leader() and not ceph_utils.is_dashboard_enabled():
|
||||
ceph_utils.mgr_enable_dashboard()
|
||||
ceph_utils.mgr_config_set(
|
||||
'mgr/dashboard/{hostname}/server_addr'.format(
|
||||
hostname=socket.gethostname()),
|
||||
str(self._get_bind_ip()))
|
||||
self.update_status()
|
||||
|
||||
def _get_bind_ip(self) -> str:
|
||||
"""Return the IP to bind the dashboard to"""
|
||||
binding = self.model.get_binding('public')
|
||||
return str(binding.network.ingress_address)
|
||||
|
||||
def _on_tls_server_config_ready(self, _) -> None:
|
||||
"""Configure TLS."""
|
||||
self.TLS_KEY_PATH.write_bytes(
|
||||
self.ca_client.server_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=serialization.NoEncryption()))
|
||||
self.TLS_CERT_PATH.write_bytes(
|
||||
self.ca_client.server_certificate.public_bytes(
|
||||
encoding=serialization.Encoding.PEM))
|
||||
self.TLS_CA_CERT_PATH.write_bytes(
|
||||
self.ca_client.ca_certificate.public_bytes(
|
||||
encoding=serialization.Encoding.PEM) +
|
||||
self.ca_client.root_ca_chain.public_bytes(
|
||||
encoding=serialization.Encoding.PEM))
|
||||
|
||||
hostname = socket.gethostname()
|
||||
subprocess.check_call(['update-ca-certificates'])
|
||||
ceph_utils.dashboard_set_ssl_certificate(
|
||||
self.TLS_CERT_PATH,
|
||||
hostname=hostname)
|
||||
ceph_utils.dashboard_set_ssl_certificate_key(
|
||||
self.TLS_KEY_PATH,
|
||||
hostname=hostname)
|
||||
if self.unit.is_leader():
|
||||
ceph_utils.mgr_config_set(
|
||||
'mgr/dashboard/standby_behaviour',
|
||||
'redirect')
|
||||
ceph_utils.mgr_config_set(
|
||||
'mgr/dashboard/ssl',
|
||||
'true')
|
||||
# Set the ssl artifacte without the hostname which appears to
|
||||
# be required even though they aren't used.
|
||||
ceph_utils.dashboard_set_ssl_certificate(
|
||||
self.TLS_CERT_PATH)
|
||||
ceph_utils.dashboard_set_ssl_certificate_key(
|
||||
self.TLS_KEY_PATH)
|
||||
self.kick_dashboard()
|
||||
|
||||
def _gen_user_password(self, length: int = 8) -> str:
|
||||
"""Generate a password"""
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
return ''.join(secrets.choice(alphabet) for i in range(length))
|
||||
|
||||
def _add_user_action(self, event: ActionEvent) -> None:
|
||||
"""Create a user"""
|
||||
username = event.params["username"]
|
||||
role = event.params["role"]
|
||||
if not all([username, role]):
|
||||
event.fail("Config missing")
|
||||
else:
|
||||
password = self._gen_user_password()
|
||||
with tempfile.NamedTemporaryFile(mode='w', delete=True) as fp:
|
||||
fp.write(password)
|
||||
fp.flush()
|
||||
cmd_out = subprocess.check_output([
|
||||
'ceph', 'dashboard', 'ac-user-create', '--enabled',
|
||||
'-i', fp.name, username, role]).decode('UTF-8')
|
||||
if re.match('User.*already exists', cmd_out):
|
||||
event.fail("User already exists")
|
||||
else:
|
||||
event.set_results({"password": password})
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(CephDashboardCharm)
|
64
src/interface_api_endpoints.py
Normal file
64
src/interface_api_endpoints.py
Normal file
@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
|
||||
from ops.framework import (
|
||||
StoredState,
|
||||
EventBase,
|
||||
ObjectEvents,
|
||||
EventSource,
|
||||
Object)
|
||||
|
||||
|
||||
class EndpointDataEvent(EventBase):
|
||||
pass
|
||||
|
||||
|
||||
class APIEndpointsEvents(ObjectEvents):
|
||||
ep_ready = EventSource(EndpointDataEvent)
|
||||
|
||||
|
||||
class APIEndpointsRequires(Object):
|
||||
|
||||
on = APIEndpointsEvents()
|
||||
_stored = StoredState()
|
||||
|
||||
def __init__(self, charm, relation_name, config_dict):
|
||||
super().__init__(charm, relation_name)
|
||||
self.config_dict = config_dict
|
||||
self.relation_name = relation_name
|
||||
self.framework.observe(
|
||||
charm.on[self.relation_name].relation_changed,
|
||||
self._on_relation_changed)
|
||||
|
||||
def _on_relation_changed(self, event):
|
||||
"""Handle the relation-changed event."""
|
||||
event.relation.data[self.model.unit]['endpoints'] = json.dumps(
|
||||
self.config_dict['endpoints'])
|
||||
|
||||
def update_config(self, config_dict):
|
||||
"""Allow for updates to relation."""
|
||||
self.config_dict = config_dict
|
||||
relation = self.model.get_relation(self.relation_name)
|
||||
if relation:
|
||||
relation.data[self.model.unit]['endpoints'] = json.dumps(
|
||||
self.config_dict['endpoints'])
|
||||
|
||||
|
||||
class APIEndpointsProvides(Object):
|
||||
|
||||
on = APIEndpointsEvents()
|
||||
_stored = StoredState()
|
||||
|
||||
def __init__(self, charm):
|
||||
super().__init__(charm, "loadbalancer")
|
||||
# Observe the relation-changed hook event and bind
|
||||
# self.on_relation_changed() to handle the event.
|
||||
self.framework.observe(
|
||||
charm.on["loadbalancer"].relation_changed,
|
||||
self._on_relation_changed)
|
||||
self.charm = charm
|
||||
|
||||
def _on_relation_changed(self, event):
|
||||
"""Handle a change to the loadbalancer relation."""
|
||||
self.on.ep_ready.emit()
|
43
src/interface_dashboard.py
Normal file
43
src/interface_dashboard.py
Normal file
@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import logging
|
||||
|
||||
from ops.framework import (
|
||||
StoredState,
|
||||
EventBase,
|
||||
ObjectEvents,
|
||||
EventSource,
|
||||
Object)
|
||||
|
||||
|
||||
class MonReadyEvent(EventBase):
|
||||
pass
|
||||
|
||||
|
||||
class CephDashboardEvents(ObjectEvents):
|
||||
mon_ready = EventSource(MonReadyEvent)
|
||||
|
||||
|
||||
class CephDashboardRequires(Object):
|
||||
|
||||
on = CephDashboardEvents()
|
||||
_stored = StoredState()
|
||||
READY_KEY = 'mon-ready'
|
||||
|
||||
def __init__(self, charm, relation_name):
|
||||
super().__init__(charm, relation_name)
|
||||
self.relation_name = relation_name
|
||||
self.framework.observe(
|
||||
charm.on[relation_name].relation_changed,
|
||||
self.on_changed)
|
||||
|
||||
def on_changed(self, event):
|
||||
logging.debug("CephDashboardRequires on_changed")
|
||||
for u in self.dashboard_relation.units:
|
||||
if self.dashboard_relation.data[u].get(self.READY_KEY) == 'True':
|
||||
logging.debug("Emitting mon ready")
|
||||
self.on.mon_ready.emit()
|
||||
|
||||
@property
|
||||
def dashboard_relation(self):
|
||||
return self.framework.model.get_relation(self.relation_name)
|
17
test-requirements.txt
Normal file
17
test-requirements.txt
Normal file
@ -0,0 +1,17 @@
|
||||
# This file is managed centrally. If you find the need to modify this as a
|
||||
# one-off, please don't. Intead, consult #openstack-charms and ask about
|
||||
# requirements management in charms via bot-control. Thank you.
|
||||
charm-tools>=2.4.4
|
||||
coverage>=3.6
|
||||
mock>=1.2
|
||||
flake8>=2.2.4,<=2.4.1
|
||||
pyflakes==2.1.1
|
||||
stestr>=2.2.0
|
||||
requests>=2.18.4
|
||||
psutil
|
||||
# oslo.i18n dropped py35 support
|
||||
oslo.i18n<4.0.0
|
||||
git+https://github.com/openstack-charmers/zaza.git#egg=zaza
|
||||
git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
|
||||
pytz # workaround for 14.04 pip/tox
|
||||
pyudev # for ceph-* charm unit tests (not mocked?)
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
39
tests/bundles/focal.yaml
Normal file
39
tests/bundles/focal.yaml
Normal file
@ -0,0 +1,39 @@
|
||||
local_overlay_enabled: False
|
||||
series: focal
|
||||
applications:
|
||||
ceph-osd:
|
||||
charm: cs:~openstack-charmers-next/ceph-osd
|
||||
num_units: 6
|
||||
storage:
|
||||
osd-devices: 'cinder,10G'
|
||||
options:
|
||||
osd-devices: '/dev/test-non-existent'
|
||||
ceph-mon:
|
||||
charm: cs:~gnuoy/ceph-mon-26
|
||||
num_units: 3
|
||||
options:
|
||||
monitor-count: '3'
|
||||
vault:
|
||||
num_units: 1
|
||||
charm: cs:~openstack-charmers-next/vault
|
||||
mysql-innodb-cluster:
|
||||
charm: cs:~openstack-charmers-next/mysql-innodb-cluster-79
|
||||
constraints: mem=3072M
|
||||
num_units: 3
|
||||
vault-mysql-router:
|
||||
charm: cs:~openstack-charmers-next/mysql-router
|
||||
ceph-dashboard:
|
||||
charm: ../../ceph-dashboard.charm
|
||||
options:
|
||||
public-hostname: 'ceph-dashboard.zaza.local'
|
||||
relations:
|
||||
- - 'ceph-osd:mon'
|
||||
- 'ceph-mon:osd'
|
||||
- - 'vault:shared-db'
|
||||
- 'vault-mysql-router:shared-db'
|
||||
- - 'vault-mysql-router:db-router'
|
||||
- 'mysql-innodb-cluster:db-router'
|
||||
- - 'ceph-dashboard:dashboard'
|
||||
- 'ceph-mon:dashboard'
|
||||
- - 'ceph-dashboard:certificates'
|
||||
- 'vault:certificates'
|
3
tests/bundles/overlays/local-charm-overlay.yaml.j2
Normal file
3
tests/bundles/overlays/local-charm-overlay.yaml.j2
Normal file
@ -0,0 +1,3 @@
|
||||
applications:
|
||||
ceph-dashboard:
|
||||
charm: ../../ceph-dashboard.charm
|
13
tests/tests.yaml
Normal file
13
tests/tests.yaml
Normal file
@ -0,0 +1,13 @@
|
||||
charm_name: ceph-dasboard
|
||||
gate_bundles:
|
||||
- focal
|
||||
smoke_bundles:
|
||||
- focal
|
||||
configure:
|
||||
- zaza.openstack.charm_tests.vault.setup.auto_initialize_no_validation
|
||||
tests:
|
||||
- zaza.openstack.charm_tests.ceph.dashboard.tests.CephDashboardTest
|
||||
target_deploy_status:
|
||||
vault:
|
||||
workload-status: blocked
|
||||
workload-status-message: Vault needs to be initialized
|
134
tox.ini
Normal file
134
tox.ini
Normal file
@ -0,0 +1,134 @@
|
||||
# Operator charm (with zaza): tox.ini
|
||||
|
||||
[tox]
|
||||
envlist = pep8,py3
|
||||
skipsdist = True
|
||||
# NOTE: Avoid build/test env pollution by not enabling sitepackages.
|
||||
sitepackages = False
|
||||
# NOTE: Avoid false positives by not skipping missing interpreters.
|
||||
skip_missing_interpreters = False
|
||||
# NOTES:
|
||||
# * We avoid the new dependency resolver by pinning pip < 20.3, see
|
||||
# https://github.com/pypa/pip/issues/9187
|
||||
# * Pinning dependencies requires tox >= 3.2.0, see
|
||||
# https://tox.readthedocs.io/en/latest/config.html#conf-requires
|
||||
# * It is also necessary to pin virtualenv as a newer virtualenv would still
|
||||
# lead to fetching the latest pip in the func* tox targets, see
|
||||
# https://stackoverflow.com/a/38133283
|
||||
requires = pip < 20.3
|
||||
virtualenv < 20.0
|
||||
# NOTE: https://wiki.canonical.com/engineering/OpenStack/InstallLatestToxOnOsci
|
||||
minversion = 3.2.0
|
||||
|
||||
[testenv]
|
||||
setenv = VIRTUAL_ENV={envdir}
|
||||
PYTHONHASHSEED=0
|
||||
CHARM_DIR={envdir}
|
||||
install_command =
|
||||
pip install {opts} {packages}
|
||||
commands = stestr run --slowest {posargs}
|
||||
whitelist_externals =
|
||||
git
|
||||
add-to-archive.py
|
||||
bash
|
||||
charmcraft
|
||||
passenv = HOME TERM CS_* OS_* TEST_*
|
||||
deps = -r{toxinidir}/test-requirements.txt
|
||||
|
||||
[testenv:py35]
|
||||
basepython = python3.5
|
||||
# python3.5 is irrelevant on a focal+ charm.
|
||||
commands = /bin/true
|
||||
|
||||
[testenv:py36]
|
||||
basepython = python3.6
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
|
||||
[testenv:py37]
|
||||
basepython = python3.7
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
|
||||
[testenv:py38]
|
||||
basepython = python3.8
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
|
||||
[testenv:py3]
|
||||
basepython = python3
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
|
||||
[testenv:pep8]
|
||||
basepython = python3
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
commands = flake8 {posargs} src unit_tests tests
|
||||
|
||||
[testenv:cover]
|
||||
# Technique based heavily upon
|
||||
# https://github.com/openstack/nova/blob/master/tox.ini
|
||||
basepython = python3
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
setenv =
|
||||
{[testenv]setenv}
|
||||
PYTHON=coverage run
|
||||
commands =
|
||||
coverage erase
|
||||
stestr run --slowest {posargs}
|
||||
coverage combine
|
||||
coverage html -d cover
|
||||
coverage xml -o cover/coverage.xml
|
||||
coverage report
|
||||
|
||||
[coverage:run]
|
||||
branch = True
|
||||
concurrency = multiprocessing
|
||||
parallel = True
|
||||
source =
|
||||
.
|
||||
omit =
|
||||
.tox/*
|
||||
*/charmhelpers/*
|
||||
unit_tests/*
|
||||
|
||||
[testenv:venv]
|
||||
basepython = python3
|
||||
commands = {posargs}
|
||||
|
||||
[testenv:build]
|
||||
basepython = python3
|
||||
deps =
|
||||
commands =
|
||||
charmcraft build
|
||||
|
||||
[testenv:func-noop]
|
||||
basepython = python3
|
||||
commands =
|
||||
functest-run-suite --help
|
||||
|
||||
[testenv:func]
|
||||
basepython = python3
|
||||
commands =
|
||||
functest-run-suite --keep-model
|
||||
|
||||
[testenv:func-smoke]
|
||||
basepython = python3
|
||||
commands =
|
||||
functest-run-suite --keep-model --smoke
|
||||
|
||||
[testenv:func-dev]
|
||||
basepython = python3
|
||||
commands =
|
||||
functest-run-suite --keep-model --dev
|
||||
|
||||
[testenv:func-target]
|
||||
basepython = python3
|
||||
commands =
|
||||
functest-run-suite --keep-model --bundle {posargs}
|
||||
|
||||
[flake8]
|
||||
# Ignore E902 because the unit_tests directory is missing in the built charm.
|
||||
ignore = E402,E226,E902
|
19
unit_tests/__init__.py
Normal file
19
unit_tests/__init__.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Copyright 2020 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.
|
||||
|
||||
import sys
|
||||
import mock
|
||||
|
||||
# Mock out secrets to make py35 happy.
|
||||
sys.modules['secrets'] = mock.MagicMock()
|
338
unit_tests/test_ceph_dashboard_charm.py
Normal file
338
unit_tests/test_ceph_dashboard_charm.py
Normal file
@ -0,0 +1,338 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright 2020 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.
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
|
||||
sys.path.append('lib') # noqa
|
||||
sys.path.append('src') # noqa
|
||||
|
||||
from mock import call, patch, MagicMock
|
||||
|
||||
from ops.testing import Harness, _TestingModelBackend
|
||||
from ops.model import (
|
||||
ActiveStatus,
|
||||
BlockedStatus,
|
||||
)
|
||||
from ops import framework, model
|
||||
import charm
|
||||
|
||||
TEST_CA = '''-----BEGIN CERTIFICATE-----
|
||||
MIIC8TCCAdmgAwIBAgIUAK1dgpjTc850TgQx6y3W1brByOwwDQYJKoZIhvcNAQEL
|
||||
BQAwGjEYMBYGA1UEAwwPRGl2aW5lQXV0aG9yaXR5MB4XDTIxMDYyMTExNTg1OFoX
|
||||
DTIxMDcyMTExNTg1OVowGjEYMBYGA1UEAwwPRGl2aW5lQXV0aG9yaXR5MIIBIjAN
|
||||
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA08gO8TDPARVhfVLOkYYRvCU1Rviv
|
||||
RYmy+ptA82XIHO1HvAuLQ8x/4bGxE+IMKSNIl+DIF9TMdmOCvKOBgRKsoOibZNfW
|
||||
MJIeQwff/8LMFWReAjOxcf9Bu2EqOqkLmUV72FU+Weta8r2kuFhgryqvz1rZeZzQ
|
||||
jP6OsscoY2FVt/TnvUL5cCOSTpKuQLSr8pDms3OuFIyhFkUinpGbgJQ83xQO1tRh
|
||||
MGiA87lahsLECTKXsLPyFMMPZ/QQuoDmuUHNkR2deOLcYRSWIBy23PctuV893gbM
|
||||
2sFTprWo1PKXSmFUd3lg6G5wSM2XRQAP81CTA3Hp8Fj5XCpOHa4HFQLxDwIDAQAB
|
||||
oy8wLTAaBgNVHREEEzARgg9EaXZpbmVBdXRob3JpdHkwDwYDVR0TAQH/BAUwAwEB
|
||||
/zANBgkqhkiG9w0BAQsFAAOCAQEAKsrUnYBJyyEIPXkWaemR5vmp0G+V6Xz3KvPB
|
||||
hLYKRONMba8xFwrjRv7b0DNAws8TcXXOKtRtJWbnSIMGhfVESF6ohqEdn+J1crXs
|
||||
2RpJgyF2u+l6gg9Sg2ngYMQYBkzjAHYTroO/itI4AWLPLHpgygzz8ho6ykWpDoxJ
|
||||
QfrrtHCl90zweYDhl4g2joIOJSZdd36+Nx9f2guItRMN87EZy1mOrKs94HlW9jwj
|
||||
mAfiGaYhgFn4JH2jVcZu4wVJErh4Z0A3UNNyOq4zlAq8pHa/54jerHTDB49UQbaI
|
||||
vZ5PsZhTZLy3FImSbe25xMUZNTt/2MMjsQwSjwiQuxLSuicJAA==
|
||||
-----END CERTIFICATE-----'''
|
||||
|
||||
TEST_CERT = '''-----BEGIN CERTIFICATE-----
|
||||
MIIEdjCCA16gAwIBAgIUPmsr+BnLb6Yy22Zg6hkXn1B6KZcwDQYJKoZIhvcNAQEL
|
||||
BQAwRTFDMEEGA1UEAxM6VmF1bHQgSW50ZXJtZWRpYXRlIENlcnRpZmljYXRlIEF1
|
||||
dGhvcml0eSAoY2hhcm0tcGtpLWxvY2FsKTAeFw0yMTA2MjExMTU4MzNaFw0yMjA2
|
||||
MjExMDU5MDJaMD4xPDA6BgNVBAMTM2p1anUtOGMzOTI5LXphemEtZWZjMDU2ZjE2
|
||||
NmNkLTAucHJvamVjdC5zZXJ2ZXJzdGFjazCCASIwDQYJKoZIhvcNAQEBBQADggEP
|
||||
ADCCAQoCggEBANW0NkSLH53M2Aok6lxN4qSSUDTnIWeuKsemLp7FwZn6zN7fRa4V
|
||||
utuGWbeYahdSIY6AG3w5opCyijM/+L4+HWoY5BWGFPj/U5V4CDF9jOerNDcoxKDy
|
||||
+h+CbJ324xJrCBOjMyW8wqK/lzCadQzy6DymOtK0RBJNHXsXiGWta7UMFo2AZcqM
|
||||
8OkOd0HkBeDM90dzTRSuy3pvqNBKmpwG4Hmg/ESh7VuobuHTtkD2/sGEVMGoXm7Q
|
||||
qk6Yf8POzNqdPoHzvY40uZWqL3OwedGWDrnNbH4sTYb1xB7fwBthvs+LNPUDzRXA
|
||||
NOYlKsfRrsiH9ELyMWUfarKXxg+7JelBIdECAwEAAaOCAWMwggFfMA4GA1UdDwEB
|
||||
/wQEAwIDqDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYE
|
||||
FEpYZVtgGevbnUrzWsjAXZix5zgzMEoGCCsGAQUFBwEBBD4wPDA6BggrBgEFBQcw
|
||||
AoYuaHR0cDovLzE3Mi4yMC4wLjExOTo4MjAwL3YxL2NoYXJtLXBraS1sb2NhbC9j
|
||||
YTCBgAYDVR0RBHkwd4IZY2VwaC1kYXNoYm9hcmQuemF6YS5sb2NhbIIfanVqdS04
|
||||
YzM5MjktemF6YS1lZmMwNTZmMTY2Y2QtMIIzanVqdS04YzM5MjktemF6YS1lZmMw
|
||||
NTZmMTY2Y2QtMC5wcm9qZWN0LnNlcnZlcnN0YWNrhwSsFAD9MEAGA1UdHwQ5MDcw
|
||||
NaAzoDGGL2h0dHA6Ly8xNzIuMjAuMC4xMTk6ODIwMC92MS9jaGFybS1wa2ktbG9j
|
||||
YWwvY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQBRUsmnc5fnNh1TSO1hVdpYBo6SRqdN
|
||||
VPuG3EV6QYPGnqadzGTr3uREUyZdkOUu4nhqDONMTdlfCwg744AIlY+eo2tpiNEp
|
||||
GOeFV0qZOiGRq7q2kllCTYCnh7hKCTCSN17o9QDTCL6w46cmH5OXo84BHkozdBiO
|
||||
cHPQ+uJ/VZaRCuOIlVS4Y4vTDB0LpNX2nHC/tMYL0zA5+pu+N6e8OWcCgKwObdh5
|
||||
38iuimYbbwv2QWBD+4eQUbxY0+TXlhdg42Um41N8BVdPapNAQRXIHrZJC5P6fXqX
|
||||
uoZ6TvbI2U0GSfpjScPP5D2F6tWK7/3nbA8bPLUJ1MKDofBVtrlA4PIH
|
||||
-----END CERTIFICATE-----'''
|
||||
|
||||
TEST_KEY = '''-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA1bQ2RIsfnczYCiTqXE3ipJJQNOchZ64qx6YunsXBmfrM3t9F
|
||||
rhW624ZZt5hqF1IhjoAbfDmikLKKMz/4vj4dahjkFYYU+P9TlXgIMX2M56s0NyjE
|
||||
oPL6H4JsnfbjEmsIE6MzJbzCor+XMJp1DPLoPKY60rREEk0dexeIZa1rtQwWjYBl
|
||||
yozw6Q53QeQF4Mz3R3NNFK7Lem+o0EqanAbgeaD8RKHtW6hu4dO2QPb+wYRUwahe
|
||||
btCqTph/w87M2p0+gfO9jjS5laovc7B50ZYOuc1sfixNhvXEHt/AG2G+z4s09QPN
|
||||
FcA05iUqx9GuyIf0QvIxZR9qspfGD7sl6UEh0QIDAQABAoIBAAHqAk5s3JSiQBEf
|
||||
MYYwIGaO9O70XwU5tyJgp6w+YzSI3Yrlfw9HHIxY0LbnQ5P/5VMMbLKZJY6cOsao
|
||||
vQafMc5AeNKEh+2PA+Wj1Jb04+0zSF1yHQjABGOB3I0xp+kDUmgynwOohCnHA4io
|
||||
6YF7L39TkdVPTgjH7gqrNEqM2hkeBWg1LY5QARDtz6Nj10LRtpQXjx/zwfGfzV2c
|
||||
TGpO8ArfPLS+a7LAJ+E+iSgDUX272Fd7DYAv7xRcRe8991umpqFzbY8FDigLWEdd
|
||||
3muWnRsJjricYM+2OO0QO8fyKhWCE31Dvc0xMLgrSTWoZAl8t7/WxyowevuVAm5o
|
||||
oclYFU0CgYEA4M6seEB/neaqAWMIshwIcwZWaLy7oQAQagjXbKohSAXNlYqgTuv7
|
||||
glk0P6uzeQOu0ejipwga6mQIc093WSzpG1sdT4bBysHS0b44Gx/6Cv0Jf6hmJGcU
|
||||
wNo3XV8b0rHZ+KWDCfr1dUjxCA9rR2fOTJniCh9Ng28cyhrFyZ6HaUcCgYEA81sj
|
||||
Z3ATs2uMxZePmGMWxOJqbQ+bHaoE+UG1dTQIVO//MmanJm3+o4ciH46D2QRWkYha
|
||||
4Eqb5wnPKCQjun8JDpwgkLkd0EGGG4uJ6E6YqL3I0+cs5lwMWJ9M3oOaFGGoFAoP
|
||||
V9lgz5f3yVdSChoubklS4KLeCiAojW/qX1rrKCcCgYEAuALz0YqZ6xm/1lrF52Ri
|
||||
1iQ93oV934854FFUZDHuBBIb8WgDSBaJTGzQA737rfaBxngl7isIPQucjyZgvrGw
|
||||
LSArocjgH6L/eYeGTU2jUhNFDyU8Vle5+RGld9w93fyOOqTf2e99s379LGfSnCQw
|
||||
DSt4hmiQ/iCZJCU9+Ia2uEkCgYAGsPjWPUStaEWkoTg3jnHv0/HtMcKoHCaq292r
|
||||
bVTVUQwJTL1H1zprMKoFiBuj+fSPZ9pn1GVZAvIJPoUk+Z08I5rZn91r/oE7fKi8
|
||||
FH0qFp3RBcg8RUepoCey7pdr/AttEaG+XqHE037isF33HSUtryJyPsgwKxYyXWNq
|
||||
X8ubfQKBgBwIpk7N754lN0i6V08Dadz0BlpfFYGO/ZfTmvVrPUxwehogtvpGnjhO
|
||||
xPs1epK65/vHbBtaUDExayOEIvVhVWcnaXdx3z1aw/Hr29NlOi62x4g/RRSloLZH
|
||||
08UCW9F5C8Ian6kglB5bPrZiJxcmssj7vSA+O6k9BjsO+ebaSRgk
|
||||
-----END RSA PRIVATE KEY-----'''
|
||||
|
||||
TEST_CHAIN = '''-----BEGIN CERTIFICATE-----
|
||||
MIIDADCCAeigAwIBAgIUN93XI0mOu3wkX5YureWnMImedUMwDQYJKoZIhvcNAQEL
|
||||
BQAwGjEYMBYGA1UEAwwPRGl2aW5lQXV0aG9yaXR5MB4XDTIxMDYyMzEwMzcwMFoX
|
||||
DTMyMDYwNjEwMzcwMFowRTFDMEEGA1UEAxM6VmF1bHQgSW50ZXJtZWRpYXRlIENl
|
||||
cnRpZmljYXRlIEF1dGhvcml0eSAoY2hhcm0tcGtpLWxvY2FsKTCCASIwDQYJKoZI
|
||||
hvcNAQEBBQADggEPADCCAQoCggEBAL1t5WYd7IVsfT5d4uztBhOPBA0EtrKw81Fe
|
||||
Rp2TNdPUkkKSQxOYKV6F1ndyD88Nxx1mcxwi8U28b1azTNVaPRjSLxyDCOD0L5qk
|
||||
LaFqppTWv8vLcjjlp6Ed3BLXoVMThWwMxJm/VSPuEXnWN5GrMR97Ae8vmnlrYDTF
|
||||
re67j0zjDPhkyevVQ5+pLeZ/saQtNNeal1qzfWMPDQK0COfXolXmlmZGzhap742e
|
||||
x4gE6alyYYrpTPA6CL9NbGhNovuz/LJvHN8fIdfw3jX+GW+yy312xDG+67PCW342
|
||||
VDrPcG+Vq/BhEPwL3blYgbmtNPDQ1plWJqoPqoJzbCxLesXZHP8CAwEAAaMTMBEw
|
||||
DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEARv1bBEgwlDG3PuhF
|
||||
Zt5kIeDLEnjFH2STz4LLERZXdKhTzuaV08QvYr+cL8XHi4Sop5BDkAuQq8mVC/xj
|
||||
7DoW/Lb9SnxfsCIu6ugwKLfJ2El6r23kDzTauIaovDYNSEo21yBYALsFZjzMJotJ
|
||||
XLpLklASTAdMmLP703hcgKgY8yxzS3WEXA9jekmn6z0y3+UZjIF5W9dW9gaQk0Eg
|
||||
vsLN7xzG9TmQfk1OHUj7y+cEbYr0M3Jdif/gG8Kl2SuaYUmvU6leA5+oZVF/Inle
|
||||
jdSckxCCd1rbvGd60AY5azD1pAuazijwW9Y9Icv2tS5oZI/4MN7YJEssj/ZLjEA7
|
||||
Alm0ZQ==
|
||||
-----END CERTIFICATE-----'''
|
||||
|
||||
|
||||
class CharmTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self, obj, patches):
|
||||
super().setUp()
|
||||
self.patches = patches
|
||||
self.obj = obj
|
||||
self.patch_all()
|
||||
|
||||
def patch(self, method):
|
||||
_m = patch.object(self.obj, method)
|
||||
mock = _m.start()
|
||||
self.addCleanup(_m.stop)
|
||||
return mock
|
||||
|
||||
def patch_all(self):
|
||||
for method in self.patches:
|
||||
setattr(self, method, self.patch(method))
|
||||
|
||||
|
||||
class _CephDashboardCharm(charm.CephDashboardCharm):
|
||||
|
||||
def _get_bind_ip(self):
|
||||
return '10.0.0.10'
|
||||
|
||||
|
||||
class TestCephDashboardCharmBase(CharmTestCase):
|
||||
|
||||
PATCHES = [
|
||||
'ceph_utils',
|
||||
'socket',
|
||||
'subprocess'
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super().setUp(charm, self.PATCHES)
|
||||
self.harness = Harness(
|
||||
_CephDashboardCharm,
|
||||
)
|
||||
|
||||
# BEGIN: Workaround until network_get is implemented
|
||||
class _TestingOPSModelBackend(_TestingModelBackend):
|
||||
|
||||
def network_get(self, endpoint_name, relation_id=None):
|
||||
network_data = {
|
||||
'bind-addresses': [{
|
||||
'interface-name': 'eth0',
|
||||
'addresses': [{
|
||||
'cidr': '10.0.0.0/24',
|
||||
'value': '10.0.0.10'}]}],
|
||||
'ingress-addresses': ['10.0.0.10'],
|
||||
'egress-subnets': ['10.0.0.0/24']}
|
||||
return network_data
|
||||
|
||||
self.harness._backend = _TestingOPSModelBackend(
|
||||
self.harness._unit_name, self.harness._meta)
|
||||
self.harness._model = model.Model(
|
||||
self.harness._meta,
|
||||
self.harness._backend)
|
||||
self.harness._framework = framework.Framework(
|
||||
":memory:",
|
||||
self.harness._charm_dir,
|
||||
self.harness._meta,
|
||||
self.harness._model)
|
||||
# END Workaround
|
||||
self.socket.gethostname.return_value = 'server1'
|
||||
self.socket.getfqdn.return_value = 'server1.local'
|
||||
|
||||
def test_init(self):
|
||||
self.harness.begin()
|
||||
self.assertFalse(self.harness.charm._stored.is_started)
|
||||
|
||||
def test__on_ca_available(self):
|
||||
rel_id = self.harness.add_relation('certificates', 'vault')
|
||||
self.harness.begin()
|
||||
self.harness.add_relation_unit(
|
||||
rel_id,
|
||||
'vault/0')
|
||||
self.harness.update_relation_data(
|
||||
rel_id,
|
||||
'vault/0',
|
||||
{'ingress-address': '10.0.0.3'})
|
||||
rel_data = self.harness.get_relation_data(rel_id, 'ceph-dashboard/0')
|
||||
self.assertEqual(
|
||||
rel_data['cert_requests'],
|
||||
'{"server1.local": {"sans": ["10.0.0.10", "server1"]}}')
|
||||
|
||||
def test_check_dashboard(self):
|
||||
socket_mock = MagicMock()
|
||||
self.socket.socket.return_value = socket_mock
|
||||
socket_mock.connect_ex.return_value = 0
|
||||
self.ceph_utils.is_dashboard_enabled.return_value = True
|
||||
self.harness.begin()
|
||||
self.assertEqual(
|
||||
self.harness.charm.check_dashboard(),
|
||||
ActiveStatus())
|
||||
|
||||
socket_mock.connect_ex.return_value = 1
|
||||
self.assertEqual(
|
||||
self.harness.charm.check_dashboard(),
|
||||
BlockedStatus('Dashboard not responding'))
|
||||
|
||||
socket_mock.connect_ex.return_value = 0
|
||||
self.ceph_utils.is_dashboard_enabled.return_value = False
|
||||
self.assertEqual(
|
||||
self.harness.charm.check_dashboard(),
|
||||
BlockedStatus('Dashboard is not enabled'))
|
||||
|
||||
def test_kick_dashboard(self):
|
||||
self.harness.begin()
|
||||
self.harness.charm.kick_dashboard()
|
||||
self.ceph_utils.mgr_disable_dashboard.assert_called_once_with()
|
||||
self.ceph_utils.mgr_enable_dashboard.assert_called_once_with()
|
||||
|
||||
def test__configure_dashboard(self):
|
||||
self.harness.begin()
|
||||
|
||||
self.ceph_utils.is_dashboard_enabled.return_value = True
|
||||
self.harness.set_leader(False)
|
||||
self.harness.charm._configure_dashboard(None)
|
||||
self.assertFalse(self.ceph_utils.mgr_enable_dashboard.called)
|
||||
self.ceph_utils.mgr_config_set.assert_called_once_with(
|
||||
'mgr/dashboard/server1/server_addr',
|
||||
'10.0.0.10')
|
||||
|
||||
self.ceph_utils.mgr_config_set.reset_mock()
|
||||
self.ceph_utils.is_dashboard_enabled.return_value = True
|
||||
self.harness.set_leader()
|
||||
self.harness.charm._configure_dashboard(None)
|
||||
self.assertFalse(self.ceph_utils.mgr_enable_dashboard.called)
|
||||
self.ceph_utils.mgr_config_set.assert_called_once_with(
|
||||
'mgr/dashboard/server1/server_addr',
|
||||
'10.0.0.10')
|
||||
|
||||
self.ceph_utils.mgr_config_set.reset_mock()
|
||||
self.ceph_utils.is_dashboard_enabled.return_value = False
|
||||
self.harness.set_leader()
|
||||
self.harness.charm._configure_dashboard(None)
|
||||
self.ceph_utils.mgr_enable_dashboard.assert_called_once_with()
|
||||
self.ceph_utils.mgr_config_set.assert_called_once_with(
|
||||
'mgr/dashboard/server1/server_addr',
|
||||
'10.0.0.10')
|
||||
|
||||
def test__get_bind_ip(self):
|
||||
self.harness.begin()
|
||||
self.assertEqual(
|
||||
self.harness.charm._get_bind_ip(),
|
||||
'10.0.0.10')
|
||||
|
||||
@patch('socket.gethostname')
|
||||
def test__on_tls_server_config_ready(self, _gethostname):
|
||||
mock_TLS_KEY_PATH = MagicMock()
|
||||
mock_TLS_CERT_PATH = MagicMock()
|
||||
mock_TLS_CA_CERT_PATH = MagicMock()
|
||||
_gethostname.return_value = 'server1'
|
||||
rel_id = self.harness.add_relation('certificates', 'vault')
|
||||
self.harness.begin()
|
||||
self.harness.set_leader()
|
||||
self.harness.charm.TLS_CERT_PATH = mock_TLS_CERT_PATH
|
||||
self.harness.charm.TLS_CA_CERT_PATH = mock_TLS_CA_CERT_PATH
|
||||
self.harness.charm.TLS_KEY_PATH = mock_TLS_KEY_PATH
|
||||
self.harness.add_relation_unit(
|
||||
rel_id,
|
||||
'vault/0')
|
||||
self.harness.update_relation_data(
|
||||
rel_id,
|
||||
'vault/0',
|
||||
{
|
||||
'ceph-dashboard_0.server.cert': TEST_CERT,
|
||||
'ceph-dashboard_0.server.key': TEST_KEY,
|
||||
'chain': TEST_CHAIN,
|
||||
'ca': TEST_CA})
|
||||
mock_TLS_CERT_PATH.write_bytes.assert_called_once()
|
||||
mock_TLS_CA_CERT_PATH.write_bytes.assert_called_once()
|
||||
mock_TLS_KEY_PATH.write_bytes.assert_called_once()
|
||||
self.subprocess.check_call.assert_called_once_with(
|
||||
['update-ca-certificates'])
|
||||
self.ceph_utils.dashboard_set_ssl_certificate.assert_has_calls([
|
||||
call(mock_TLS_CERT_PATH, hostname='server1'),
|
||||
call(mock_TLS_CERT_PATH)])
|
||||
self.ceph_utils.dashboard_set_ssl_certificate_key.assert_has_calls([
|
||||
call(mock_TLS_KEY_PATH, hostname='server1'),
|
||||
call(mock_TLS_KEY_PATH)])
|
||||
self.ceph_utils.mgr_config_set.assert_has_calls([
|
||||
call('mgr/dashboard/standby_behaviour', 'redirect'),
|
||||
call('mgr/dashboard/ssl', 'true')])
|
||||
self.ceph_utils.mgr_disable_dashboard.assert_called_once_with()
|
||||
self.ceph_utils.mgr_enable_dashboard.assert_called_once_with()
|
||||
|
||||
@patch.object(charm.secrets, 'choice')
|
||||
def test__gen_user_password(self, _choice):
|
||||
self.harness.begin()
|
||||
_choice.return_value = 'r'
|
||||
self.assertEqual(
|
||||
self.harness.charm._gen_user_password(),
|
||||
'rrrrrrrr')
|
||||
|
||||
@patch.object(charm.tempfile, 'NamedTemporaryFile')
|
||||
@patch.object(charm.secrets, 'choice')
|
||||
def test__add_user_action(self, _choice, _NTFile):
|
||||
self.subprocess.check_output.return_value = b''
|
||||
_NTFile.return_value.__enter__.return_value.name = 'tempfilename'
|
||||
_choice.return_value = 'r'
|
||||
self.harness.begin()
|
||||
action_event = MagicMock()
|
||||
action_event.params = {
|
||||
'username': 'auser',
|
||||
'role': 'administrator'}
|
||||
self.harness.charm._add_user_action(action_event)
|
||||
self.subprocess.check_output.assert_called_once_with(
|
||||
['ceph', 'dashboard', 'ac-user-create', '--enabled',
|
||||
'-i', 'tempfilename', 'auser', 'administrator'])
|
159
unit_tests/test_interface_api_endpoints.py
Normal file
159
unit_tests/test_interface_api_endpoints.py
Normal file
@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright 2020 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.
|
||||
|
||||
import copy
|
||||
import json
|
||||
import unittest
|
||||
import sys
|
||||
sys.path.append('lib') # noqa
|
||||
sys.path.append('src') # noqa
|
||||
from ops.testing import Harness
|
||||
from ops.charm import CharmBase
|
||||
import interface_api_endpoints
|
||||
|
||||
|
||||
class TestAPIEndpointsRequires(unittest.TestCase):
|
||||
|
||||
class MyCharm(CharmBase):
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
self.seen_events = []
|
||||
self.ingress = interface_api_endpoints.APIEndpointsRequires(
|
||||
self,
|
||||
'loadbalancer',
|
||||
{
|
||||
'endpoints': [{
|
||||
'service-type': 'ceph-dashboard',
|
||||
'frontend-port': 8443,
|
||||
'backend-port': 8443,
|
||||
'backend-ip': '10.0.0.10',
|
||||
'check-type': 'httpd'}]})
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.harness = Harness(
|
||||
self.MyCharm,
|
||||
meta='''
|
||||
name: my-charm
|
||||
requires:
|
||||
loadbalancer:
|
||||
interface: api-endpoints
|
||||
'''
|
||||
)
|
||||
self.eps = [{
|
||||
'service-type': 'ceph-dashboard',
|
||||
'frontend-port': 8443,
|
||||
'backend-port': 8443,
|
||||
'backend-ip': '10.0.0.10',
|
||||
'check-type': 'httpd'}]
|
||||
|
||||
def add_loadbalancer_relation(self):
|
||||
rel_id = self.harness.add_relation(
|
||||
'loadbalancer',
|
||||
'service-loadbalancer')
|
||||
self.harness.add_relation_unit(
|
||||
rel_id,
|
||||
'service-loadbalancer/0')
|
||||
self.harness.update_relation_data(
|
||||
rel_id,
|
||||
'service-loadbalancer/0',
|
||||
{'ingress-address': '10.0.0.3'})
|
||||
return rel_id
|
||||
|
||||
def test_init(self):
|
||||
self.harness.begin()
|
||||
self.assertEqual(
|
||||
self.harness.charm.ingress.config_dict,
|
||||
{'endpoints': self.eps})
|
||||
self.assertEqual(
|
||||
self.harness.charm.ingress.relation_name,
|
||||
'loadbalancer')
|
||||
|
||||
def test__on_relation_changed(self):
|
||||
self.harness.begin()
|
||||
rel_id = self.add_loadbalancer_relation()
|
||||
rel_data = self.harness.get_relation_data(
|
||||
rel_id,
|
||||
'my-charm/0')
|
||||
self.assertEqual(
|
||||
rel_data['endpoints'],
|
||||
json.dumps(self.eps))
|
||||
|
||||
def test_update_config(self):
|
||||
self.harness.begin()
|
||||
rel_id = self.add_loadbalancer_relation()
|
||||
new_eps = copy.deepcopy(self.eps)
|
||||
new_eps.append({
|
||||
'service-type': 'ceph-dashboard',
|
||||
'frontend-port': 9443,
|
||||
'backend-port': 9443,
|
||||
'backend-ip': '10.0.0.10',
|
||||
'check-type': 'https'})
|
||||
self.harness.charm.ingress.update_config(
|
||||
{'endpoints': new_eps})
|
||||
rel_data = self.harness.get_relation_data(
|
||||
rel_id,
|
||||
'my-charm/0')
|
||||
self.assertEqual(
|
||||
rel_data['endpoints'],
|
||||
json.dumps(new_eps))
|
||||
|
||||
|
||||
class TestAPIEndpointsProvides(unittest.TestCase):
|
||||
|
||||
class MyCharm(CharmBase):
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
self.seen_events = []
|
||||
self.api_eps = interface_api_endpoints.APIEndpointsProvides(self)
|
||||
self.framework.observe(
|
||||
self.api_eps.on.ep_ready,
|
||||
self._log_event)
|
||||
|
||||
def _log_event(self, event):
|
||||
self.seen_events.append(type(event).__name__)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.harness = Harness(
|
||||
self.MyCharm,
|
||||
meta='''
|
||||
name: my-charm
|
||||
provides:
|
||||
loadbalancer:
|
||||
interface: api-endpoints
|
||||
'''
|
||||
)
|
||||
|
||||
def test_on_changed(self):
|
||||
self.harness.begin()
|
||||
# No MonReadyEvent as relation is absent
|
||||
self.assertEqual(
|
||||
self.harness.charm.seen_events,
|
||||
[])
|
||||
rel_id = self.harness.add_relation('loadbalancer', 'ceph-dashboard')
|
||||
self.harness.add_relation_unit(
|
||||
rel_id,
|
||||
'ceph-dashboard/0')
|
||||
self.harness.update_relation_data(
|
||||
rel_id,
|
||||
'ceph-dashboard/0',
|
||||
{'ingress-address': '10.0.0.3'})
|
||||
self.assertEqual(
|
||||
self.harness.charm.seen_events,
|
||||
['EndpointDataEvent'])
|
98
unit_tests/test_interface_dashboard.py
Normal file
98
unit_tests/test_interface_dashboard.py
Normal file
@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright 2020 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.
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
sys.path.append('lib') # noqa
|
||||
sys.path.append('src') # noqa
|
||||
from ops.testing import Harness
|
||||
from ops.charm import CharmBase, CharmMeta
|
||||
import interface_dashboard
|
||||
|
||||
|
||||
class MyCharm(CharmBase):
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
self.framework.meta = CharmMeta.from_yaml(metadata='''
|
||||
name: my-charm
|
||||
requires:
|
||||
dashboard:
|
||||
interface: ceph-dashboard
|
||||
scope: container
|
||||
''')
|
||||
|
||||
self.seen_events = []
|
||||
self.mon = interface_dashboard.CephDashboardRequires(
|
||||
self,
|
||||
'dashboard')
|
||||
|
||||
self.framework.observe(
|
||||
self.mon.on.mon_ready,
|
||||
self._log_event)
|
||||
|
||||
def _log_event(self, event):
|
||||
self.seen_events.append(type(event).__name__)
|
||||
|
||||
|
||||
class TestCephDashboardRequires(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.harness = Harness(
|
||||
MyCharm,
|
||||
)
|
||||
|
||||
def add_dashboard_relation(self):
|
||||
rel_id = self.harness.add_relation('dashboard', 'ceph-mon')
|
||||
self.harness.add_relation_unit(
|
||||
rel_id,
|
||||
'ceph-mon/0')
|
||||
return rel_id
|
||||
|
||||
def test_relation_name(self):
|
||||
self.harness.begin()
|
||||
self.assertEqual(
|
||||
self.harness.charm.mon.relation_name,
|
||||
'dashboard')
|
||||
|
||||
def test_dashboard_relation(self):
|
||||
self.harness.begin()
|
||||
self.assertIsNone(
|
||||
self.harness.charm.mon.dashboard_relation)
|
||||
rel_id = self.add_dashboard_relation()
|
||||
self.assertEqual(
|
||||
self.harness.charm.mon.dashboard_relation.id,
|
||||
rel_id)
|
||||
|
||||
def test_on_changed(self):
|
||||
self.harness.begin()
|
||||
# No MonReadyEvent as relation is absent
|
||||
self.assertEqual(
|
||||
self.harness.charm.seen_events,
|
||||
[])
|
||||
rel_id = self.add_dashboard_relation()
|
||||
# No MonReadyEvent as ceph-mon has not declared it is ready.
|
||||
self.assertEqual(
|
||||
self.harness.charm.seen_events,
|
||||
[])
|
||||
self.harness.update_relation_data(
|
||||
rel_id,
|
||||
'ceph-mon/0',
|
||||
{'mon-ready': 'True'})
|
||||
self.assertEqual(
|
||||
self.harness.charm.seen_events,
|
||||
['MonReadyEvent'])
|
Loading…
Reference in New Issue
Block a user