diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 8ef84fc..0000000 --- a/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 99 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/.jujuignore b/.jujuignore deleted file mode 100644 index 6ccd559..0000000 --- a/.jujuignore +++ /dev/null @@ -1,3 +0,0 @@ -/venv -*.py[cod] -*.charm diff --git a/.stestr.conf b/.stestr.conf deleted file mode 100644 index e4750de..0000000 --- a/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/.zuul.yaml b/.zuul.yaml index 0a41ba8..e7c200a 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -1,11 +1,3 @@ - project: templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: ovn-central-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false + - noop-jobs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 0c55b9c..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,55 +0,0 @@ -# ovn-central-k8s - -## Developing - -Create and activate a virtualenv with the development requirements: - - virtualenv -p python3 venv - source venv/bin/activate - pip install -r requirements-dev.txt - -## Code overview - -Get familiarise with [Charmed Operator Framework](https://juju.is/docs/sdk) -and [Sunbeam documentation](sunbeam-docs). - -ovn-central-k8s charm uses the ops_sunbeam library and extends -OSBaseOperatorAPICharm from the library. - -ovn-central-k8s charm consumes certificates to get generated -certificates from vault and provides ovsdb-cms relation to -provide ovn-central endpoints for external services to connect to. - -ovn-central-k8s starts northd, ovsdb-sb-server, ovsdb-nb-server -services by creating separate pebble handlers for each service. - -## Intended use case - -ovn-central-k8s charm deploys and configures OVN Central services -on a kubernetes based environment. - -## Roadmap - -TODO - -## Testing - -The Python operator framework includes a very nice harness for testing -operator behaviour without full deployment. Run tests using command: - - tox -e py3 - -## Deployment - -This project uses tox for building and managing. To build the charm -run: - - tox -e build - -To deploy the local test instance: - - juju deploy ./ovn-central-k8s_ubuntu-20.04-amd64.charm --trust --resource ovn-northd-image=registry.jujucharms.com/charm/kau2b145zhaeuj5ly4w4m30qiq8qzspf93tnd/ovn-northd-image ovn-nb-db-server-image=registry.jujucharms.com/charm/kau2b145zhaeuj5ly4w4m30qiq8qzspf93tnd/ovn-nb-db-server-image ovn-sb-db-server-image=registry.jujucharms.com/charm/kau2b145zhaeuj5ly4w4m30qiq8qzspf93tnd/ovn-sb-db-server-image - - - -[sunbeam-docs]: https://opendev.org/openstack/charm-ops-sunbeam/src/branch/main/README.rst diff --git a/LICENSE b/LICENSE deleted file mode 100644 index d645695..0000000 --- a/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - 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. diff --git a/README.md b/README.md deleted file mode 100644 index 4c79ee0..0000000 --- a/README.md +++ /dev/null @@ -1,70 +0,0 @@ -# ovn-central-k8s - -## Description - -ovn-central-k8s is an operator to manage the OVN central -services (northd, ovsdb-nb, ovsdb-sb) on a Kubernetes based -environment. - -## Usage - -### Deployment - -ovn-central-k8s is deployed using below command: - - juju deploy ovn-central-k8s ovn-central - -Now connect the ovn-central operator to Vault to generate -certificates for secure authentication and communication: - - juju relate vault:certificates ovn-central:certificates - -### Configuration - -This section covers common and/or important configuration options. See file -`config.yaml` for the full list of options, along with their descriptions and -default values. See the [Juju documentation][juju-docs-config-apps] for details -on configuring applications. - -### Actions - -This section covers Juju [actions][juju-docs-actions] supported by the charm. -Actions allow specific operations to be performed on a per-unit basis. To -display action descriptions run `juju actions ovn-central`. If the charm is not -deployed then see file `actions.yaml`. - -## Relations - -ovn-central-k8s requires the following relations: - -`certificates`: To retrieve generated certificates from vault - -ovn-central-k8s provides the following relations: - -`ovsdb-cms`: Used by Neutron to get ovn-central IPs. - -## OCI Images - -The charm by default uses following images: - - `registry.jujucharms.com/charm/kau2b145zhaeuj5ly4w4m30qiq8qzspf93tnd/ovn-northd-image` - `registry.jujucharms.com/charm/kau2b145zhaeuj5ly4w4m30qiq8qzspf93tnd/ovn-nb-db-server-image` - `registry.jujucharms.com/charm/kau2b145zhaeuj5ly4w4m30qiq8qzspf93tnd/ovn-sb-db-server-image` - -## Contributing - -Please see the [Juju SDK docs](https://juju.is/docs/sdk) for guidelines -on enhancements to this charm following best practice guidelines, and -[CONTRIBUTING.md](contributors-guide) for developer guidance. - -## Bugs - -Please report bugs on [Launchpad][lp-bugs-charm-ovn-central-k8s]. - - - - -[contributors-guide]: https://opendev.org/x/charm-ovn-central-k8s/src/branch/main/CONTRIBUTING.md -[juju-docs-actions]: https://jaas.ai/docs/actions -[juju-docs-config-apps]: https://juju.is/docs/configuring-applications -[lp-bugs-charm-ovn-central-k8s]: https://bugs.launchpad.net/charm-ovn-central-k8s/+filebug diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..39cdc03 --- /dev/null +++ b/README.rst @@ -0,0 +1,12 @@ +This repository has been merged into the +`Sunbeam Charms `_ +repository. + +The contents of this repository are still available in the Git +source code management system. To see the contents of this +repository before it reached its end of life, please check out the +previous commit with "git checkout HEAD^1". + +For any further questions, please email +openstack-discuss@lists.openstack.org or join #openstack-sunbeam on +OFTC. diff --git a/actions.yaml b/actions.yaml deleted file mode 100644 index 88e6195..0000000 --- a/actions.yaml +++ /dev/null @@ -1,2 +0,0 @@ -# NOTE: no actions yet! -{ } diff --git a/charmcraft.yaml b/charmcraft.yaml deleted file mode 100644 index eea1df0..0000000 --- a/charmcraft.yaml +++ /dev/null @@ -1,32 +0,0 @@ -# Learn more about charmcraft.yaml configuration at: -# https://juju.is/docs/sdk/charmcraft-config -type: "charm" -bases: - - build-on: - - name: "ubuntu" - channel: "22.04" - run-on: - - name: "ubuntu" - channel: "22.04" -parts: - update-certificates: - plugin: nil - override-build: | - apt update - apt install -y ca-certificates - update-ca-certificates - - charm: - after: [update-certificates] - build-packages: - - git - - libffi-dev - - libssl-dev - - pkg-config - - rustc - - cargo - charm-binary-python-packages: - - cryptography - - jsonschema - - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/config.yaml b/config.yaml deleted file mode 100644 index b6b6e8f..0000000 --- a/config.yaml +++ /dev/null @@ -1,29 +0,0 @@ -options: - ovsdb-server-election-timer: - default: 4 - type: int - description: | - Raft leader election timeout in seconds. The charm allows a value between - 1 and 60 seconds. - . - The Open vSwitch ovsdb-server default of 1 second may not be sufficient - for a loaded cluster where the database server may be too busy serving - requests to respond to elections in time. - . - Using a higher value will increase the time to discover a real failure, - but you must weigh that against the risk of spurious leader flapping and - the unwanted churn that entails. - . - NOTE: The ovsdb-server will refuse to decrease or increase the value of - this timer more than 2x the current value. The charm will compensate for - this and decrease / increase the timer in increments, but care should be - taken to not decrease / increase the value too much in one operation. - ovsdb-server-inactivity-probe: - default: 60 - type: int - description: | - Maximum number of seconds of idle time on connection to client before - sending an inactivity probe message. - - The Open vSwitch ovsdb-server default of 5 seconds may not be sufficient - depending on type and load of the CMS you want to connect to OVN. diff --git a/fetch-libs.sh b/fetch-libs.sh deleted file mode 100755 index 2b40c7b..0000000 --- a/fetch-libs.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates diff --git a/lib/charms/ovn_central_k8s/v0/ovsdb.py b/lib/charms/ovn_central_k8s/v0/ovsdb.py deleted file mode 100644 index 732679a..0000000 --- a/lib/charms/ovn_central_k8s/v0/ovsdb.py +++ /dev/null @@ -1,206 +0,0 @@ -"""TODO: Add a proper docstring here. - -This is a placeholder docstring for this charm library. Docstrings are -presented on Charmhub and updated whenever you push a new version of the -library. - -Complete documentation about creating and documenting libraries can be found -in the SDK docs at https://juju.is/docs/sdk/libraries. - -See `charmcraft publish-lib` and `charmcraft fetch-lib` for details of how to -share and consume charm libraries. They serve to enhance collaboration -between charmers. Use a charmer's libraries for classes that handle -integration with their charm. - -Bear in mind that new revisions of the different major API versions (v0, v1, -v2 etc) are maintained independently. You can continue to update v0 and v1 -after you have pushed v3. - -Markdown is supported, following the CommonMark specification. -""" - -import logging -import typing -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -# The unique Charmhub library identifier, never change it -LIBID = "114b7bb1970445daa61650e451f9da62" - -# Increment this major API version when introducing breaking changes -LIBAPI = 0 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 3 - - -# TODO: add your code here! Happy coding! -class OVSDBCMSConnectedEvent(EventBase): - """OVSDBCMS connected Event.""" - - pass - - -class OVSDBCMSReadyEvent(EventBase): - """OVSDBCMS ready for use Event.""" - - pass - - -class OVSDBCMSGoneAwayEvent(EventBase): - """OVSDBCMS relation has gone-away Event""" - - pass - - -class OVSDBCMSServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(OVSDBCMSConnectedEvent) - ready = EventSource(OVSDBCMSReadyEvent) - goneaway = EventSource(OVSDBCMSGoneAwayEvent) - - -class OVSDBCMSRequires(Object): - """ - OVSDBCMSRequires class - """ - - on = OVSDBCMSServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_ovsdb_cms_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_ovsdb_cms_relation_broken, - ) - - def _on_ovsdb_cms_relation_joined(self, event): - """OVSDBCMS relation joined.""" - logging.debug("OVSDBCMSRequires on_joined") - self.on.connected.emit() - - def bound_hostnames(self): - return self.get_all_unit_values("bound-hostname") - - def bound_addresses(self): - return self.get_all_unit_values("bound-address") - - def remote_ready(self): - return all(self.bound_hostnames()) or all(self.bound_addresses()) - - def _on_ovsdb_cms_relation_changed(self, event): - """OVSDBCMS relation changed.""" - logging.debug("OVSDBCMSRequires on_changed") - if self.remote_ready(): - self.on.ready.emit() - - def _on_ovsdb_cms_relation_broken(self, event): - """OVSDBCMS relation broken.""" - logging.debug("OVSDBCMSRequires on_broken") - self.on.goneaway.emit() - - def get_all_unit_values(self, key: str) -> typing.List[str]: - """Retrieve value for key from all related units.""" - values = [] - relation = self.framework.model.get_relation(self.relation_name) - if relation: - for unit in relation.units: - values.append(relation.data[unit].get(key)) - return values - - - -class OVSDBCMSClientConnectedEvent(EventBase): - """OVSDBCMS connected Event.""" - - pass - - -class OVSDBCMSClientReadyEvent(EventBase): - """OVSDBCMS ready for use Event.""" - - pass - - -class OVSDBCMSClientGoneAwayEvent(EventBase): - """OVSDBCMS relation has gone-away Event""" - - pass - - -class OVSDBCMSClientEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(OVSDBCMSClientConnectedEvent) - ready = EventSource(OVSDBCMSClientReadyEvent) - goneaway = EventSource(OVSDBCMSClientGoneAwayEvent) - - -class OVSDBCMSProvides(Object): - """ - OVSDBCMSProvides class - """ - - on = OVSDBCMSClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_ovsdb_cms_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_ovsdb_cms_relation_broken, - ) - - def _on_ovsdb_cms_relation_joined(self, event): - """Handle ovsdb-cms joined.""" - logging.debug("OVSDBCMSProvides on_joined") - self.on.connected.emit() - - def _on_ovsdb_cms_relation_changed(self, event): - """Handle ovsdb-cms changed.""" - logging.debug("OVSDBCMSProvides on_changed") - self.on.ready.emit() - - def _on_ovsdb_cms_relation_broken(self, event): - """Handle ovsdb-cms broken.""" - logging.debug("OVSDBCMSProvides on_departed") - self.on.goneaway.emit() - - def set_unit_data(self, settings: typing.Dict[str, str]) -> None: - """Publish settings on the peer unit data bag.""" - relations = self.framework.model.relations[self.relation_name] - for relation in relations: - for k, v in settings.items(): - relation.data[self.model.unit][k] = v diff --git a/lib/charms/tls_certificates_interface/v1/tls_certificates.py b/lib/charms/tls_certificates_interface/v1/tls_certificates.py deleted file mode 100644 index 1eda19b..0000000 --- a/lib/charms/tls_certificates_interface/v1/tls_certificates.py +++ /dev/null @@ -1,1261 +0,0 @@ -# Copyright 2021 Canonical Ltd. -# See LICENSE file for licensing details. - -"""Library for the tls-certificates relation. - -This library contains the Requires and Provides classes for handling the tls-certificates -interface. - -## Getting Started -From a charm directory, fetch the library using `charmcraft`: - -```shell -charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates -``` - -Add the following libraries to the charm's `requirements.txt` file: -- jsonschema -- cryptography - -Add the following section to the charm's `charmcraft.yaml` file: -```yaml -parts: - charm: - build-packages: - - libffi-dev - - libssl-dev - - rustc - - cargo -``` - -### Provider charm -The provider charm is the charm providing certificates to another charm that requires them. In -this example, the provider charm is storing its private key using a peer relation interface called -`replicas`. - -Example: -```python -from charms.tls_certificates_interface.v1.tls_certificates import ( - CertificateCreationRequestEvent, - CertificateRevocationRequestEvent, - TLSCertificatesProvidesV1, - generate_private_key, -) -from ops.charm import CharmBase, InstallEvent -from ops.main import main -from ops.model import ActiveStatus, WaitingStatus - - -def generate_ca(private_key: bytes, subject: str) -> str: - return "whatever ca content" - - -def generate_certificate(ca: str, private_key: str, csr: str) -> str: - return "Whatever certificate" - - -class ExampleProviderCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - self.certificates = TLSCertificatesProvidesV1(self, "certificates") - self.framework.observe( - self.certificates.on.certificate_request, self._on_certificate_request - ) - self.framework.observe( - self.certificates.on.certificate_revoked, self._on_certificate_revocation_request - ) - self.framework.observe(self.on.install, self._on_install) - - def _on_install(self, event: InstallEvent) -> None: - private_key_password = b"banana" - private_key = generate_private_key(password=private_key_password) - ca_certificate = generate_ca(private_key=private_key, subject="whatever") - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update( - { - "private_key_password": "banana", - "private_key": private_key, - "ca_certificate": ca_certificate, - } - ) - self.unit.status = ActiveStatus() - - def _on_certificate_request(self, event: CertificateCreationRequestEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - ca_certificate = replicas_relation.data[self.app].get("ca_certificate") - private_key = replicas_relation.data[self.app].get("private_key") - certificate = generate_certificate( - ca=ca_certificate, - private_key=private_key, - csr=event.certificate_signing_request, - ) - - self.certificates.set_relation_certificate( - certificate=certificate, - certificate_signing_request=event.certificate_signing_request, - ca=ca_certificate, - chain=[ca_certificate, certificate], - relation_id=event.relation_id, - ) - - def _on_certificate_revocation_request(self, event: CertificateRevocationRequestEvent) -> None: - # Do what you want to do with this information - pass - - -if __name__ == "__main__": - main(ExampleProviderCharm) -``` - -### Requirer charm -The requirer charm is the charm requiring certificates from another charm that provides them. In -this example, the requirer charm is storing its certificates using a peer relation interface called -`replicas`. - -Example: -```python -from charms.tls_certificates_interface.v1.tls_certificates import ( - CertificateAvailableEvent, - CertificateExpiringEvent, - TLSCertificatesRequiresV1, - generate_csr, - generate_private_key, -) -from ops.charm import CharmBase, RelationJoinedEvent -from ops.main import main -from ops.model import ActiveStatus, WaitingStatus - - -class ExampleRequirerCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - self.cert_subject = "whatever" - self.certificates = TLSCertificatesRequiresV1(self, "certificates") - self.framework.observe(self.on.install, self._on_install) - self.framework.observe( - self.on.certificates_relation_joined, self._on_certificates_relation_joined - ) - self.framework.observe( - self.certificates.on.certificate_available, self._on_certificate_available - ) - self.framework.observe( - self.certificates.on.certificate_expiring, self._on_certificate_expiring - ) - - def _on_install(self, event) -> None: - private_key_password = b"banana" - private_key = generate_private_key(password=private_key_password) - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update( - {"private_key_password": "banana", "private_key": private_key.decode()} - ) - - def _on_certificates_relation_joined(self, event: RelationJoinedEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - replicas_relation.data[self.app].update({"csr": csr.decode()}) - self.certificates.request_certificate_creation(certificate_signing_request=csr) - - def _on_certificate_available(self, event: CertificateAvailableEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update({"certificate": event.certificate}) - replicas_relation.data[self.app].update({"ca": event.ca}) - replicas_relation.data[self.app].update({"chain": event.chain}) - self.unit.status = ActiveStatus() - - def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - old_csr = replicas_relation.data[self.app].get("csr") - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - new_csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - self.certificates.request_certificate_renewal( - old_certificate_signing_request=old_csr, - new_certificate_signing_request=new_csr, - ) - replicas_relation.data[self.app].update({"csr": new_csr.decode()}) - - -if __name__ == "__main__": - main(ExampleRequirerCharm) -``` -""" # noqa: D405, D410, D411, D214, D416 - -import copy -import json -import logging -import uuid -from datetime import datetime, timedelta -from ipaddress import IPv4Address -from typing import Dict, List, Optional - -from cryptography import x509 -from cryptography.hazmat._oid import ExtensionOID -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.hazmat.primitives.serialization import pkcs12 -from cryptography.x509.extensions import Extension, ExtensionNotFound -from jsonschema import exceptions, validate # type: ignore[import] -from ops.charm import CharmBase, CharmEvents, RelationChangedEvent, UpdateStatusEvent -from ops.framework import EventBase, EventSource, Handle, Object - -# The unique Charmhub library identifier, never change it -LIBID = "afd8c2bccf834997afce12c2706d2ede" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 10 - -REQUIRER_JSON_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/requirer.json", # noqa: E501 - "type": "object", - "title": "`tls_certificates` requirer root schema", - "description": "The `tls_certificates` root schema comprises the entire requirer databag for this interface.", # noqa: E501 - "examples": [ - { - "certificate_signing_requests": [ - { - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\\n-----END CERTIFICATE REQUEST-----\\n" # noqa: E501 - }, - { - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBAMk3raaX803cHvzlBF9LC7KORT46z4VjyU5PIaMb\\nQLIDgYKFYI0n5hf2Ra4FAHvOvEmW7bjNlHORFEmvnpcU5kPMNUyKFMTaC8LGmN8z\\nUBH3aK+0+FRvY4afn9tgj5435WqOG9QdoDJ0TJkjJbJI9M70UOgL711oU7ql6HxU\\n4d2ydFK9xAHrBwziNHgNZ72L95s4gLTXf0fAHYf15mDA9U5yc+YDubCKgTXzVySQ\\nUx73VCJLfC/XkZIh559IrnRv5G9fu6BMLEuBwAz6QAO4+/XidbKWN4r2XSq5qX4n\\n6EPQQWP8/nd4myq1kbg6Q8w68L/0YdfjCmbyf2TuoWeImdUCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQBIdwraBvpYo/rl5MH1+1Um6HRg4gOdQPY5WcJy9B9tgzJz\\nittRSlRGTnhyIo6fHgq9KHrmUthNe8mMTDailKFeaqkVNVvk7l0d1/B90Kz6OfmD\\nxN0qjW53oP7y3QB5FFBM8DjqjmUnz5UePKoX4AKkDyrKWxMwGX5RoET8c/y0y9jp\\nvSq3Wh5UpaZdWbe1oVY8CqMVUEVQL2DPjtopxXFz2qACwsXkQZxWmjvZnRiP8nP8\\nbdFaEuh9Q6rZ2QdZDEtrU4AodPU3NaukFr5KlTUQt3w/cl+5//zils6G5zUWJ2pN\\ng7+t9PTvXHRkH+LnwaVnmsBFU2e05qADQbfIn7JA\\n-----END CERTIFICATE REQUEST-----\\n" # noqa: E501 - }, - ] - } - ], - "properties": { - "certificate_signing_requests": { - "type": "array", - "items": { - "type": "object", - "properties": {"certificate_signing_request": {"type": "string"}}, - "required": ["certificate_signing_request"], - }, - } - }, - "required": ["certificate_signing_requests"], - "additionalProperties": True, -} - -PROVIDER_JSON_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/provider.json", # noqa: E501 - "type": "object", - "title": "`tls_certificates` provider root schema", - "description": "The `tls_certificates` root schema comprises the entire provider databag for this interface.", # noqa: E501 - "example": [ - { - "certificates": [ - { - "ca": "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n", # noqa: E501 - "chain": [ - "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n" # noqa: E501, W505 - ], - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\n-----END CERTIFICATE REQUEST-----\n", # noqa: E501 - "certificate": "-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n", # noqa: E501 - } - ] - } - ], - "properties": { - "certificates": { - "$id": "#/properties/certificates", - "type": "array", - "items": { - "$id": "#/properties/certificates/items", - "type": "object", - "required": ["certificate_signing_request", "certificate", "ca", "chain"], - "properties": { - "certificate_signing_request": { - "$id": "#/properties/certificates/items/certificate_signing_request", - "type": "string", - }, - "certificate": { - "$id": "#/properties/certificates/items/certificate", - "type": "string", - }, - "ca": {"$id": "#/properties/certificates/items/ca", "type": "string"}, - "chain": { - "$id": "#/properties/certificates/items/chain", - "type": "array", - "items": { - "type": "string", - "$id": "#/properties/certificates/items/chain/items", - }, - }, - }, - "additionalProperties": True, - }, - } - }, - "required": ["certificates"], - "additionalProperties": True, -} - - -logger = logging.getLogger(__name__) - - -class CertificateAvailableEvent(EventBase): - """Charm Event triggered when a TLS certificate is available.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - - -class CertificateExpiringEvent(EventBase): - """Charm Event triggered when a TLS certificate is almost expired.""" - - def __init__(self, handle, certificate: str, expiry: str): - """CertificateExpiringEvent. - - Args: - handle (Handle): Juju framework handle - certificate (str): TLS Certificate - expiry (str): Datetime string reprensenting the time at which the certificate - won't be valid anymore. - """ - super().__init__(handle) - self.certificate = certificate - self.expiry = expiry - - def snapshot(self) -> dict: - """Returns snapshot.""" - return {"certificate": self.certificate, "expiry": self.expiry} - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.expiry = snapshot["expiry"] - - -class CertificateExpiredEvent(EventBase): - """Charm Event triggered when a TLS certificate is expired.""" - - def __init__(self, handle: Handle, certificate: str): - super().__init__(handle) - self.certificate = certificate - - def snapshot(self) -> dict: - """Returns snapshot.""" - return {"certificate": self.certificate} - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - - -class CertificateCreationRequestEvent(EventBase): - """Charm Event triggered when a TLS certificate is required.""" - - def __init__(self, handle: Handle, certificate_signing_request: str, relation_id: int): - super().__init__(handle) - self.certificate_signing_request = certificate_signing_request - self.relation_id = relation_id - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate_signing_request": self.certificate_signing_request, - "relation_id": self.relation_id, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.relation_id = snapshot["relation_id"] - - -class CertificateRevocationRequestEvent(EventBase): - """Charm Event triggered when a TLS certificate needs to be revoked.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: str, - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - - -def _load_relation_data(raw_relation_data: dict) -> dict: - """Loads relation data from the relation data bag. - - Json loads all data. - - Args: - raw_relation_data: Relation data from the databag - - Returns: - dict: Relation data in dict format. - """ - certificate_data = dict() - for key in raw_relation_data: - try: - certificate_data[key] = json.loads(raw_relation_data[key]) - except (json.decoder.JSONDecodeError, TypeError): - certificate_data[key] = raw_relation_data[key] - return certificate_data - - -def generate_ca( - private_key: bytes, - subject: str, - private_key_password: Optional[bytes] = None, - validity: int = 365, - country: str = "US", -) -> bytes: - """Generates a CA Certificate. - - Args: - private_key (bytes): Private key - subject (str): Certificate subject - private_key_password (bytes): Private key password - validity (int): Certificate validity time (in days) - country (str): Certificate Issuing country - - Returns: - bytes: CA Certificate. - """ - private_key_object = serialization.load_pem_private_key( - private_key, password=private_key_password - ) - subject = issuer = x509.Name( - [ - x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country), - x509.NameAttribute(x509.NameOID.COMMON_NAME, subject), - ] - ) - subject_identifier_object = x509.SubjectKeyIdentifier.from_public_key( - private_key_object.public_key() # type: ignore[arg-type] - ) - subject_identifier = key_identifier = subject_identifier_object.public_bytes() - cert = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(private_key_object.public_key()) # type: ignore[arg-type] - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.utcnow()) - .not_valid_after(datetime.utcnow() + timedelta(days=validity)) - .add_extension(x509.SubjectKeyIdentifier(digest=subject_identifier), critical=False) - .add_extension( - x509.AuthorityKeyIdentifier( - key_identifier=key_identifier, - authority_cert_issuer=None, - authority_cert_serial_number=None, - ), - critical=False, - ) - .add_extension( - x509.BasicConstraints(ca=True, path_length=None), - critical=True, - ) - .sign(private_key_object, hashes.SHA256()) # type: ignore[arg-type] - ) - return cert.public_bytes(serialization.Encoding.PEM) - - -def generate_certificate( - csr: bytes, - ca: bytes, - ca_key: bytes, - ca_key_password: Optional[bytes] = None, - validity: int = 365, - alt_names: List[str] = None, -) -> bytes: - """Generates a TLS certificate based on a CSR. - - Args: - csr (bytes): CSR - ca (bytes): CA Certificate - ca_key (bytes): CA private key - ca_key_password: CA private key password - validity (int): Certificate validity (in days) - alt_names (list): List of alt names to put on cert - prefer putting SANs in CSR - - Returns: - bytes: Certificate - """ - csr_object = x509.load_pem_x509_csr(csr) - subject = csr_object.subject - issuer = x509.load_pem_x509_certificate(ca).issuer - private_key = serialization.load_pem_private_key(ca_key, password=ca_key_password) - - certificate_builder = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(csr_object.public_key()) - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.utcnow()) - .not_valid_after(datetime.utcnow() + timedelta(days=validity)) - ) - - extensions_list = csr_object.extensions - san_ext: Optional[x509.Extension] = None - if alt_names: - full_sans_dns = alt_names.copy() - try: - loaded_san_ext = csr_object.extensions.get_extension_for_class( - x509.SubjectAlternativeName - ) - full_sans_dns.extend(loaded_san_ext.value.get_values_for_type(x509.DNSName)) - except ExtensionNotFound: - pass - finally: - san_ext = Extension( - ExtensionOID.SUBJECT_ALTERNATIVE_NAME, - False, - x509.SubjectAlternativeName([x509.DNSName(name) for name in full_sans_dns]), - ) - if not extensions_list: - extensions_list = x509.Extensions([san_ext]) - - for extension in extensions_list: - if extension.value.oid == ExtensionOID.SUBJECT_ALTERNATIVE_NAME and san_ext: - extension = san_ext - - certificate_builder = certificate_builder.add_extension( - extension.value, - critical=extension.critical, - ) - certificate_builder._version = x509.Version.v3 - cert = certificate_builder.sign(private_key, hashes.SHA256()) # type: ignore[arg-type] - return cert.public_bytes(serialization.Encoding.PEM) - - -def generate_pfx_package( - certificate: bytes, - private_key: bytes, - package_password: str, - private_key_password: Optional[bytes] = None, -) -> bytes: - """Generates a PFX package to contain the TLS certificate and private key. - - Args: - certificate (bytes): TLS certificate - private_key (bytes): Private key - package_password (str): Password to open the PFX package - private_key_password (bytes): Private key password - - Returns: - bytes: - """ - private_key_object = serialization.load_pem_private_key( - private_key, password=private_key_password - ) - certificate_object = x509.load_pem_x509_certificate(certificate) - name = certificate_object.subject.rfc4514_string() - pfx_bytes = pkcs12.serialize_key_and_certificates( - name=name.encode(), - cert=certificate_object, - key=private_key_object, # type: ignore[arg-type] - cas=None, - encryption_algorithm=serialization.BestAvailableEncryption(package_password.encode()), - ) - return pfx_bytes - - -def generate_private_key( - password: Optional[bytes] = None, - key_size: int = 2048, - public_exponent: int = 65537, -) -> bytes: - """Generates a private key. - - Args: - password (bytes): Password for decrypting the private key - key_size (int): Key size in bytes - public_exponent: Public exponent. - - Returns: - bytes: Private Key - """ - private_key = rsa.generate_private_key( - public_exponent=public_exponent, - key_size=key_size, - ) - key_bytes = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.BestAvailableEncryption(password) - if password - else serialization.NoEncryption(), - ) - return key_bytes - - -def generate_csr( - private_key: bytes, - subject: str, - add_unique_id_to_subject_name: bool = True, - organization: str = None, - email_address: str = None, - country_name: str = None, - private_key_password: Optional[bytes] = None, - sans: Optional[List[str]] = None, - sans_oid: Optional[List[str]] = None, - sans_ip: Optional[List[str]] = None, - sans_dns: Optional[List[str]] = None, - additional_critical_extensions: Optional[List] = None, -) -> bytes: - """Generates a CSR using private key and subject. - - Args: - private_key (bytes): Private key - subject (str): CSR Subject. - add_unique_id_to_subject_name (bool): Whether a unique ID must be added to the CSR's - subject name. Always leave to "True" when the CSR is used to request certificates - using the tls-certificates relation. - organization (str): Name of organization. - email_address (str): Email address. - country_name (str): Country Name. - private_key_password (bytes): Private key password - sans (list): Use sans_dns - this will be deprecated in a future release - List of DNS subject alternative names (keeping it for now for backward compatibility) - sans_oid (list): List of registered ID SANs - sans_dns (list): List of DNS subject alternative names (similar to the arg: sans) - sans_ip (list): List of IP subject alternative names - additional_critical_extensions (list): List if critical additional extension objects. - Object must be a x509 ExtensionType. - - Returns: - bytes: CSR - """ - signing_key = serialization.load_pem_private_key(private_key, password=private_key_password) - subject_name = [x509.NameAttribute(x509.NameOID.COMMON_NAME, subject)] - if add_unique_id_to_subject_name: - unique_identifier = uuid.uuid4() - subject_name.append( - x509.NameAttribute(x509.NameOID.X500_UNIQUE_IDENTIFIER, str(unique_identifier)) - ) - if organization: - subject_name.append(x509.NameAttribute(x509.NameOID.ORGANIZATION_NAME, organization)) - if email_address: - subject_name.append(x509.NameAttribute(x509.NameOID.EMAIL_ADDRESS, email_address)) - if country_name: - subject_name.append(x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country_name)) - csr = x509.CertificateSigningRequestBuilder(subject_name=x509.Name(subject_name)) - - _sans: List[x509.GeneralName] = [] - if sans_oid: - _sans.extend([x509.RegisteredID(x509.ObjectIdentifier(san)) for san in sans_oid]) - if sans_ip: - _sans.extend([x509.IPAddress(IPv4Address(san)) for san in sans_ip]) - if sans: - _sans.extend([x509.DNSName(san) for san in sans]) - if sans_dns: - _sans.extend([x509.DNSName(san) for san in sans_dns]) - if _sans: - csr = csr.add_extension(x509.SubjectAlternativeName(set(_sans)), critical=False) - - if additional_critical_extensions: - for extension in additional_critical_extensions: - csr = csr.add_extension(extension, critical=True) - - signed_certificate = csr.sign(signing_key, hashes.SHA256()) # type: ignore[arg-type] - return signed_certificate.public_bytes(serialization.Encoding.PEM) - - -class CertificatesProviderCharmEvents(CharmEvents): - """List of events that the TLS Certificates provider charm can leverage.""" - - certificate_creation_request = EventSource(CertificateCreationRequestEvent) - certificate_revocation_request = EventSource(CertificateRevocationRequestEvent) - - -class CertificatesRequirerCharmEvents(CharmEvents): - """List of events that the TLS Certificates requirer charm can leverage.""" - - certificate_available = EventSource(CertificateAvailableEvent) - certificate_expiring = EventSource(CertificateExpiringEvent) - certificate_expired = EventSource(CertificateExpiredEvent) - - -class TLSCertificatesProvidesV1(Object): - """TLS certificates provider class to be instantiated by TLS certificates providers.""" - - on = CertificatesProviderCharmEvents() - - def __init__(self, charm: CharmBase, relationship_name: str): - super().__init__(charm, relationship_name) - self.framework.observe( - charm.on[relationship_name].relation_changed, self._on_relation_changed - ) - self.charm = charm - self.relationship_name = relationship_name - - def _add_certificate( - self, - relation_id: int, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - ) -> None: - """Adds certificate to relation data. - - Args: - relation_id (int): Relation id - certificate (str): Certificate - certificate_signing_request (str): Certificate Signing Request - ca (str): CA Certificate - chain (list): CA Chain - - Returns: - None - """ - relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - new_certificate = { - "certificate": certificate, - "certificate_signing_request": certificate_signing_request, - "ca": ca, - "chain": chain, - } - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = provider_relation_data.get("certificates", []) - certificates = copy.deepcopy(provider_certificates) - if new_certificate in certificates: - logger.info("Certificate already in relation data - Doing nothing") - return - certificates.append(new_certificate) - relation.data[self.model.app]["certificates"] = json.dumps(certificates) - - def _remove_certificate( - self, - relation_id: int, - certificate: str = None, - certificate_signing_request: str = None, - ) -> None: - """Removes certificate from a given relation based on user provided certificate or csr. - - Args: - relation_id (int): Relation id - certificate (str): Certificate (optional) - certificate_signing_request: Certificate signing request (optional) - - Returns: - None - """ - relation = self.model.get_relation( - relation_name=self.relationship_name, - relation_id=relation_id, - ) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} with relation id {relation_id} does not exist" - ) - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = provider_relation_data.get("certificates", []) - certificates = copy.deepcopy(provider_certificates) - for certificate_dict in certificates: - if certificate and certificate_dict["certificate"] == certificate: - certificates.remove(certificate_dict) - if ( - certificate_signing_request - and certificate_dict["certificate_signing_request"] == certificate_signing_request - ): - certificates.remove(certificate_dict) - relation.data[self.model.app]["certificates"] = json.dumps(certificates) - - @staticmethod - def _relation_data_is_valid(certificates_data: dict) -> bool: - """Uses JSON schema validator to validate relation data content. - - Args: - certificates_data (dict): Certificate data dictionary as retrieved from relation data. - - Returns: - bool: True/False depending on whether the relation data follows the json schema. - """ - try: - validate(instance=certificates_data, schema=REQUIRER_JSON_SCHEMA) - return True - except exceptions.ValidationError: - return False - - def revoke_all_certificates(self) -> None: - """Revokes all certificates of this provider. - - This method is meant to be used when the Root CA has changed. - """ - for relation in self.model.relations[self.relationship_name]: - relation.data[self.model.app]["certificates"] = json.dumps([]) - - def set_relation_certificate( - self, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - relation_id: int, - ) -> None: - """Adds certificates to relation data. - - Args: - certificate (str): Certificate - certificate_signing_request (str): Certificate signing request - ca (str): CA Certificate - chain (list): CA Chain - relation_id (int): Juju relation ID - - Returns: - None - """ - certificates_relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - self._remove_certificate( - certificate_signing_request=certificate_signing_request.strip(), - relation_id=relation_id, - ) - self._add_certificate( - relation_id=relation_id, - certificate=certificate.strip(), - certificate_signing_request=certificate_signing_request.strip(), - ca=ca.strip(), - chain=[cert.strip() for cert in chain], - ) - - def remove_certificate(self, certificate: str) -> None: - """Removes a given certificate from relation data. - - Args: - certificate (str): TLS Certificate - - Returns: - None - """ - certificates_relation = self.model.relations[self.relationship_name] - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - for certificate_relation in certificates_relation: - self._remove_certificate(certificate=certificate, relation_id=certificate_relation.id) - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handler triggerred on relation changed event. - - Looks at the relation data and either emits: - - certificate request event: If the unit relation data contains a CSR for which - a certificate does not exist in the provider relation data. - - certificate revocation event: If the provider relation data contains a CSR for which - a csr does not exist in the requirer relation data. - - Args: - event: Juju event - - Returns: - None - """ - assert event.unit is not None - requirer_relation_data = _load_relation_data(event.relation.data[event.unit]) - provider_relation_data = _load_relation_data(event.relation.data[self.charm.app]) - if not self._relation_data_is_valid(requirer_relation_data): - logger.warning( - f"Relation data did not pass JSON Schema validation: {requirer_relation_data}" - ) - return - provider_certificates = provider_relation_data.get("certificates", []) - requirer_csrs = requirer_relation_data.get("certificate_signing_requests", []) - provider_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in provider_certificates - ] - requirer_unit_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in requirer_csrs - ] - for certificate_signing_request in requirer_unit_csrs: - if certificate_signing_request not in provider_csrs: - self.on.certificate_creation_request.emit( - certificate_signing_request=certificate_signing_request, - relation_id=event.relation.id, - ) - self._revoke_certificates_for_which_no_csr_exists(relation_id=event.relation.id) - - def _revoke_certificates_for_which_no_csr_exists(self, relation_id: int) -> None: - """Revokes certificates for which no unit has a CSR. - - Goes through all generated certificates and compare agains the list of CSRS for all units - of a given relationship. - - Args: - relation_id (int): Relation id - - Returns: - None - """ - certificates_relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - provider_relation_data = _load_relation_data(certificates_relation.data[self.charm.app]) - list_of_csrs: List[str] = [] - for unit in certificates_relation.units: - requirer_relation_data = _load_relation_data(certificates_relation.data[unit]) - requirer_csrs = requirer_relation_data.get("certificate_signing_requests", []) - list_of_csrs.extend(csr["certificate_signing_request"] for csr in requirer_csrs) - provider_certificates = provider_relation_data.get("certificates", []) - for certificate in provider_certificates: - if certificate["certificate_signing_request"] not in list_of_csrs: - self.on.certificate_revocation_request.emit( - certificate=certificate["certificate"], - certificate_signing_request=certificate["certificate_signing_request"], - ca=certificate["ca"], - chain=certificate["chain"], - ) - self.remove_certificate(certificate=certificate["certificate"]) - - -class TLSCertificatesRequiresV1(Object): - """TLS certificates requirer class to be instantiated by TLS certificates requirers.""" - - on = CertificatesRequirerCharmEvents() - - def __init__( - self, - charm: CharmBase, - relationship_name: str, - expiry_notification_time: int = 168, - ): - """Generates/use private key and observes relation changed event. - - Args: - charm: Charm object - relationship_name: Juju relation name - expiry_notification_time (int): Time difference between now and expiry (in hours). - Used to trigger the CertificateExpiring event. Default: 7 days. - """ - super().__init__(charm, relationship_name) - self.relationship_name = relationship_name - self.charm = charm - self.expiry_notification_time = expiry_notification_time - self.framework.observe( - charm.on[relationship_name].relation_changed, self._on_relation_changed - ) - self.framework.observe(charm.on.update_status, self._on_update_status) - - @property - def _requirer_csrs(self) -> List[Dict[str, str]]: - """Returns list of requirer CSR's from relation data.""" - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - requirer_relation_data = _load_relation_data(relation.data[self.model.unit]) - return requirer_relation_data.get("certificate_signing_requests", []) - - @property - def _provider_certificates(self) -> List[Dict[str, str]]: - """Returns list of provider CSR's from relation data.""" - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - if not relation.app: - raise RuntimeError(f"Remote app for relation {self.relationship_name} does not exist") - provider_relation_data = _load_relation_data(relation.data[relation.app]) - return provider_relation_data.get("certificates", []) - - def _add_requirer_csr(self, csr: str) -> None: - """Adds CSR to relation data. - - Args: - csr (str): Certificate Signing Request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - new_csr_dict = {"certificate_signing_request": csr} - if new_csr_dict in self._requirer_csrs: - logger.info("CSR already in relation data - Doing nothing") - return - requirer_csrs = copy.deepcopy(self._requirer_csrs) - requirer_csrs.append(new_csr_dict) - relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs) - - def _remove_requirer_csr(self, csr: str) -> None: - """Removes CSR from relation data. - - Args: - csr (str): Certificate signing request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - requirer_csrs = copy.deepcopy(self._requirer_csrs) - csr_dict = {"certificate_signing_request": csr} - if csr_dict not in requirer_csrs: - logger.info("CSR not in relation data - Doing nothing") - return - requirer_csrs.remove(csr_dict) - relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs) - - def request_certificate_creation(self, certificate_signing_request: bytes) -> None: - """Request TLS certificate to provider charm. - - Args: - certificate_signing_request (bytes): Certificate Signing Request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - message = ( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - logger.error(message) - raise RuntimeError(message) - self._add_requirer_csr(certificate_signing_request.decode().strip()) - logger.info("Certificate request sent to provider") - - def request_certificate_revocation(self, certificate_signing_request: bytes) -> None: - """Removes CSR from relation data. - - The provider of this relation is then expected to remove certificates associated to this - CSR from the relation data as well and emit a request_certificate_revocation event for the - provider charm to interpret. - - Args: - certificate_signing_request (bytes): Certificate Signing Request - - Returns: - None - """ - self._remove_requirer_csr(certificate_signing_request.decode().strip()) - logger.info("Certificate revocation sent to provider") - - def request_certificate_renewal( - self, old_certificate_signing_request: bytes, new_certificate_signing_request: bytes - ) -> None: - """Renews certificate. - - Removes old CSR from relation data and adds new one. - - Args: - old_certificate_signing_request: Old CSR - new_certificate_signing_request: New CSR - - Returns: - None - """ - try: - self.request_certificate_revocation( - certificate_signing_request=old_certificate_signing_request - ) - except RuntimeError: - logger.warning("Certificate revocation failed.") - self.request_certificate_creation( - certificate_signing_request=new_certificate_signing_request - ) - logger.info("Certificate renewal request completed.") - - @staticmethod - def _relation_data_is_valid(certificates_data: dict) -> bool: - """Checks whether relation data is valid based on json schema. - - Args: - certificates_data: Certificate data in dict format. - - Returns: - bool: Whether relation data is valid. - """ - try: - validate(instance=certificates_data, schema=PROVIDER_JSON_SCHEMA) - return True - except exceptions.ValidationError: - return False - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handler triggerred on relation changed events. - - Args: - event: Juju event - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - logger.warning(f"No relation: {self.relationship_name}") - return - if not relation.app: - logger.warning(f"No remote app in relation: {self.relationship_name}") - return - provider_relation_data = _load_relation_data(relation.data[relation.app]) - if not self._relation_data_is_valid(provider_relation_data): - logger.warning( - f"Provider relation data did not pass JSON Schema validation: " - f"{event.relation.data[relation.app]}" - ) - return - requirer_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in self._requirer_csrs - ] - for certificate in self._provider_certificates: - if certificate["certificate_signing_request"] in requirer_csrs: - self.on.certificate_available.emit( - certificate_signing_request=certificate["certificate_signing_request"], - certificate=certificate["certificate"], - ca=certificate["ca"], - chain=certificate["chain"], - ) - - def _on_update_status(self, event: UpdateStatusEvent) -> None: - """Triggered on update status event. - - Goes through each certificate in the "certificates" relation and checks their expiry date. - If they are close to expire (<7 days), emits a CertificateExpiringEvent event and if - they are expired, emits a CertificateExpiredEvent. - - Args: - event (UpdateStatusEvent): Juju event - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - logger.warning(f"No relation: {self.relationship_name}") - return - if not relation.app: - logger.warning(f"No remote app in relation: {self.relationship_name}") - return - provider_relation_data = _load_relation_data(relation.data[relation.app]) - if not self._relation_data_is_valid(provider_relation_data): - logger.warning( - f"Provider relation data did not pass JSON Schema validation: " - f"{relation.data[relation.app]}" - ) - return - for certificate_dict in self._provider_certificates: - certificate = certificate_dict["certificate"] - try: - certificate_object = x509.load_pem_x509_certificate(data=certificate.encode()) - except ValueError: - logger.warning("Could not load certificate.") - continue - time_difference = certificate_object.not_valid_after - datetime.utcnow() - if time_difference.total_seconds() < 0: - logger.warning("Certificate is expired") - self.on.certificate_expired.emit(certificate=certificate) - self.request_certificate_revocation(certificate.encode()) - continue - if time_difference.total_seconds() < (self.expiry_notification_time * 60 * 60): - logger.warning("Certificate almost expired") - self.on.certificate_expiring.emit( - certificate=certificate, expiry=certificate_object.not_valid_after.isoformat() - ) diff --git a/metadata.yaml b/metadata.yaml deleted file mode 100644 index 33c3dc4..0000000 --- a/metadata.yaml +++ /dev/null @@ -1,83 +0,0 @@ -name: ovn-central-k8s -summary: Open Virtual Network for Open vSwitch -maintainer: OpenStack Charmers -description: | - Principal charm that deploys ovn-northd, the OVN central control daemon, - and ovsdb-server, the Open vSwitch Database (OVSDB). - - The ovn-northd daemon is responsible for translating the high-level OVN - configuration into logical configuration consumable by daemons such as - ovn-controller. - - The ovn-northd process talks to OVN Northbound- and Southbound- databases. - - The ovsdb-server exposes endpoints over relations implemented by the ovsdb - interface. - - The charm supports clustering of the OVSDB, you must have a odd number of - units for this to work. Note that write performance decreases as you - increase the number of units. - - Running multiple ovn-northd daemons is supported and they will operate in - active/passive mode. The daemon uses a locking feature in the OVSDB to - automatically choose a single active instance. -tags: -- networking -source: https://opendev.org/x/charm-ovn-central-k8s -issues: https://bugs.launchpad.net/charm-ovn-central-k8s -bases: - - name: ubuntu - channel: 22.04/stable -assumes: - - k8s-api - - juju >= 3.1 -storage: - databases: - type: filesystem - description: | - Persistent storage for OVN Northbound and Southbound databases - minimum-size: 100M - -containers: - ovn-sb-db-server: - resource: ovn-sb-db-server-image - mounts: - - storage: databases - location: /var/lib/ovn - ovn-nb-db-server: - resource: ovn-nb-db-server-image - mounts: - - storage: databases - location: /var/lib/ovn - ovn-northd: - resource: ovn-northd-image - -resources: - ovn-sb-db-server-image: - type: oci-image - description: OCI image for OVN Southbound Database Server - upstream-source: ghcr.io/canonical/ovn-consolidated:23.09 - ovn-nb-db-server-image: - type: oci-image - description: OCI image for OVN Northbound Database Server - upstream-source: ghcr.io/canonical/ovn-consolidated:23.09 - ovn-northd-image: - type: oci-image - description: OCI image for OVN Northd - upstream-source: ghcr.io/canonical/ovn-consolidated:23.09 - -requires: - certificates: - interface: tls-certificates - -provides: - ovsdb: - interface: ovsdb - ovsdb-cms: - interface: ovsdb-cms - ovsdb-server: - interface: ovsdb-cluster - -peers: - peers: - interface: ovn-central-peer diff --git a/osci.yaml b/osci.yaml deleted file mode 100644 index 15e9a47..0000000 --- a/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: ovn-central-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 23.09/edge diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 2896bc0..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/rename.sh b/rename.sh deleted file mode 100755 index d0c35c9..0000000 --- a/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 9211d66..0000000 --- a/requirements.txt +++ /dev/null @@ -1,14 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -cryptography -jinja2 -jsonschema -lightkube -lightkube-models -ops - -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/src/charm.py b/src/charm.py deleted file mode 100755 index ad0971e..0000000 --- a/src/charm.py +++ /dev/null @@ -1,498 +0,0 @@ -#!/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 - -"""OVN Central Operator Charm. - -This charm provide Glance services as part of an OpenStack deployment -""" - -import logging -from typing import ( - List, - Mapping, -) - -import charms.ovn_central_k8s.v0.ovsdb as ovsdb -import ops.charm -import ops.pebble -import ops_sunbeam.charm as sunbeam_charm -import ops_sunbeam.config_contexts as sunbeam_ctxts -import ops_sunbeam.core as sunbeam_core -import ops_sunbeam.guard as sunbeam_guard -import ops_sunbeam.ovn.config_contexts as ovn_ctxts -import ops_sunbeam.ovn.container_handlers as ovn_chandlers -import ops_sunbeam.ovn.relation_handlers as ovn_rhandlers -import ops_sunbeam.relation_handlers as sunbeam_rhandlers -import tenacity -from ops.framework import ( - StoredState, -) -from ops.main import ( - main, -) - -import ovn -import ovsdb as ch_ovsdb - -logger = logging.getLogger(__name__) - -OVN_SB_DB_CONTAINER = "ovn-sb-db-server" -OVN_NB_DB_CONTAINER = "ovn-nb-db-server" -OVN_NORTHD_CONTAINER = "ovn-northd" -OVN_DB_CONTAINERS = [OVN_SB_DB_CONTAINER, OVN_NB_DB_CONTAINER] - - -class OVNNorthBPebbleHandler(ovn_chandlers.OVNPebbleHandler): - """Handler for North OVN DB.""" - - @property - def wrapper_script(self): - """Wrapper script for managing OVN service.""" - return "/root/ovn-northd-wrapper.sh" - - @property - def status_command(self): - """Status command for container.""" - return "/usr/share/ovn/scripts/ovn-ctl status_northd" - - @property - def service_description(self): - """Description of service.""" - return "OVN Northd" - - def default_container_configs(self): - """Config files for container.""" - _cc = super().default_container_configs() - _cc.append( - sunbeam_core.ContainerConfigFile( - "/etc/ovn/ovn-northd-db-params.conf", "root", "root" - ) - ) - return _cc - - -class OVNNorthBDBPebbleHandler(ovn_chandlers.OVNPebbleHandler): - """Handler for North-bound OVN DB.""" - - @property - def wrapper_script(self): - """Wrapper script for managing OVN service.""" - return "/root/ovn-nb-db-server-wrapper.sh" - - @property - def status_command(self): - """Status command for container.""" - # This command always return 0 even if the DB service - # is not running, so adding healthcheck with tcp check - return "/usr/share/ovn/scripts/ovn-ctl status_ovsdb" - - @property - def service_description(self): - """Description of service.""" - return "OVN North Bound DB" - - def default_container_configs(self): - """Config files for container.""" - _cc = super().default_container_configs() - _cc.append( - sunbeam_core.ContainerConfigFile( - "/root/ovn-nb-cluster-join.sh", "root", "root" - ) - ) - return _cc - - def get_healthcheck_layer(self) -> dict: - """Health check pebble layer. - - :returns: pebble health check layer configuration for OVN NB DB - :rtype: dict - """ - return { - "checks": { - "online": { - "override": "replace", - "level": "ready", - "tcp": {"port": 6641}, - }, - } - } - - -class OVNSouthBDBPebbleHandler(ovn_chandlers.OVNPebbleHandler): - """Handler for South-bound OVN DB.""" - - @property - def wrapper_script(self): - """Wrapper script for managing OVN service.""" - return "/root/ovn-sb-db-server-wrapper.sh" - - @property - def status_command(self): - """Status command for container.""" - # This command always return 0 even if the DB service - # is not running, so adding healthcheck with tcp check - return "/usr/share/ovn/scripts/ovn-ctl status_ovsdb" - - @property - def service_description(self): - """Description of service.""" - return "OVN South Bound DB" - - def default_container_configs(self): - """Config files for container.""" - _cc = super().default_container_configs() - _cc.append( - sunbeam_core.ContainerConfigFile( - "/root/ovn-sb-cluster-join.sh", "root", "root" - ) - ) - return _cc - - def get_healthcheck_layer(self) -> dict: - """Health check pebble layer. - - :returns: pebble health check layer configuration for OVN SB DB - :rtype: dict - """ - return { - "checks": { - "online": { - "override": "replace", - "level": "ready", - "tcp": {"port": 6642}, - }, - } - } - - -class OVNCentralOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S): - """Charm the service.""" - - _state = StoredState() - mandatory_relations = {"certificates", "peers"} - - def __init__(self, framework): - """Setup OVN central charm class.""" - super().__init__(framework) - - def get_pebble_handlers(self): - """Pebble handlers for all OVN containers.""" - pebble_handlers = [ - OVNNorthBPebbleHandler( - self, - OVN_NORTHD_CONTAINER, - "ovn-northd", - self.container_configs, - self.template_dir, - self.configure_charm, - ), - OVNSouthBDBPebbleHandler( - self, - OVN_SB_DB_CONTAINER, - "ovn-sb-db-server", - self.container_configs, - self.template_dir, - self.configure_charm, - ), - OVNNorthBDBPebbleHandler( - self, - OVN_NB_DB_CONTAINER, - "ovn-nb-db-server", - self.container_configs, - self.template_dir, - self.configure_charm, - ), - ] - return pebble_handlers - - def get_relation_handlers( - self, handlers=None - ) -> List[sunbeam_rhandlers.RelationHandler]: - """Relation handlers for the service.""" - handlers = handlers or [] - if self.can_add_handler("peers", handlers): - self.peers = ovn_rhandlers.OVNDBClusterPeerHandler( - self, - "peers", - self.configure_charm, - "peers" in self.mandatory_relations, - ) - handlers.append(self.peers) - if self.can_add_handler("ovsdb-cms", handlers): - self.ovsdb_cms = ovn_rhandlers.OVSDBCMSProvidesHandler( - self, - "ovsdb-cms", - self.configure_charm, - "ovsdb-cms" in self.mandatory_relations, - ) - handlers.append(self.ovsdb_cms) - handlers = super().get_relation_handlers(handlers) - return handlers - - @property - def config_contexts(self) -> List[sunbeam_ctxts.ConfigContext]: - """Configuration contexts for the operator.""" - contexts = super().config_contexts - contexts.append(ovn_ctxts.OVNDBConfigContext(self, "ovs_db")) - return contexts - - @property - def databases(self) -> Mapping[str, str]: - """Databases needed to support this charm. - - Return empty dict as no mysql databases are - required. - """ - return {} - - def ovn_rundir(self): - """OVN run dir.""" - return "/var/run/ovn" - - def get_pebble_executor(self, container_name): - """Execute command in pebble.""" - container = self.unit.get_container(container_name) - - def _run_via_pebble(*args): - process = container.exec(list(args), timeout=5 * 60) - out, warnings = process.wait_output() - if warnings: - for line in warnings.splitlines(): - logger.warning("CMD Out: %s", line.strip()) - return out - - return _run_via_pebble - - @tenacity.retry( - stop=tenacity.stop_after_attempt(3), - retry=tenacity.retry_if_exception_type(ops.pebble.ExecError), - after=tenacity.after_log(logger, logging.WARNING), - wait=tenacity.wait_exponential(multiplier=1, min=5, max=30), - ) - def cluster_status(self, db, cmd_executor): - """OVN version agnostic cluster_status helper. - - :param db: Database to operate on - :type db: str - :returns: Object describing the cluster status or None - :rtype: Optional[ch_ovn.OVNClusterStatus] - """ - try: - # The charm will attempt to retrieve cluster status before OVN - # is clustered and while units are paused, so we need to handle - # errors from this call gracefully. - return ovn.cluster_status( - db, rundir=self.ovn_rundir(), cmd_executor=cmd_executor - ) - except ValueError as e: - logging.error( - "Unable to get cluster status, ovsdb-server " - "not ready yet?: {}".format(e) - ) - return - - def configure_ovn_listener(self, db, port_map): - """Create or update OVN listener configuration. - - :param db: Database to operate on, 'nb' or 'sb' - :type db: str - :param port_map: Dictionary with port number and associated settings - :type port_map: Dict[int,Dict[str,str]] - :raises: ValueError - """ - if db == "nb": - executor = self.get_pebble_executor(OVN_NB_DB_CONTAINER) - elif db == "sb": - executor = self.get_pebble_executor(OVN_SB_DB_CONTAINER) - status = self.cluster_status( - "ovn{}_db".format(db), cmd_executor=executor - ) - if status and status.is_cluster_leader: - logging.debug( - "configure_ovn_listener is_cluster_leader {}".format(db) - ) - connections = ch_ovsdb.SimpleOVSDB( - "ovn-{}ctl".format(db), cmd_executor=executor - ).connection - for port, settings in port_map.items(): - logging.debug("port {} {}".format(port, settings)) - # discover and create any non-existing listeners first - for connection in connections.find( - 'target="pssl:{}"'.format(port) - ): - logging.debug("Found port {}".format(port)) - break - else: - logging.debug("Create port {}".format(port)) - executor( - "ovn-{}ctl".format(db), - "--", - "--id=@connection", - "create", - "connection", - 'target="pssl:{}"'.format(port), - "--", - "add", - "{}_Global".format(db.upper()), - ".", - "connections", - "@connection", - ) - # set/update connection settings - for connection in connections.find( - 'target="pssl:{}"'.format(port) - ): - for k, v in settings.items(): - logging.debug( - "set {} {} {}".format( - str(connection["_uuid"]), k, v - ) - ) - connections.set(str(connection["_uuid"]), k, v) - - def check_leader_ready(self): - """Check leader is ready and has supplied mandatory data.""" - if self.supports_peer_relation and not ( - self.unit.is_leader() or self.is_leader_ready() - ): - raise sunbeam_guard.WaitingExceptionError("Leader not ready") - missing_leader_data = [ - k for k in ["nb_cid", "sb_cid"] if not self.leader_get(k) - ] - if missing_leader_data: - logging.debug(f"missing {missing_leader_data} from leader") - self.unit.status = ops.model.WaitingStatus( - "Waiting for data from leader" - ) - raise sunbeam_guard.WaitingExceptionError( - "Missing data from leader" - ) - - def start_northd(self): - """Start northd service.""" - ph = self.get_named_pebble_handler(OVN_NORTHD_CONTAINER) - ph.start_service() - - def configure_app_leader(self, event): - """Run global app setup. - - These are tasks that should only be run once per application and only - the leader runs them. - """ - # Start services in North/South containers on lead unit - logging.debug("Starting services in DB containers") - for ph in self.get_named_pebble_handlers(OVN_DB_CONTAINERS): - ph.start_service() - # Attempt to setup listers etc - self.configure_ovn() - nb_status = self.cluster_status( - "ovnnb_db", self.get_pebble_executor(OVN_NB_DB_CONTAINER) - ) - sb_status = self.cluster_status( - "ovnsb_db", self.get_pebble_executor(OVN_SB_DB_CONTAINER) - ) - logging.debug("Telling peers leader is ready and cluster ids") - self.set_leader_ready() - self.leader_set( - { - "nb_cid": str(nb_status.cluster_id), - "sb_cid": str(sb_status.cluster_id), - } - ) - self.set_leader_ready() - self.start_northd() - self.check_pebble_handlers_ready() - - def configure_app_non_leader(self, event): - """Configure non leader.""" - if not self.peers.expected_peers_available(): - raise sunbeam_guard.WaitingExceptionError( - "Expected peer units not ready, deferring cluster join" - ) - - logging.debug("Attempting to join OVN_Northbound cluster") - container = self.unit.get_container(OVN_NB_DB_CONTAINER) - process = container.exec( - ["bash", "/root/ovn-nb-cluster-join.sh"], timeout=5 * 60 - ) - out, warnings = process.wait_output() - if warnings: - for line in warnings.splitlines(): - logger.warning("CMD Out: %s", line.strip()) - - logging.debug("Attempting to join OVN_Southbound cluster") - container = self.unit.get_container(OVN_SB_DB_CONTAINER) - process = container.exec( - ["bash", "/root/ovn-sb-cluster-join.sh"], timeout=5 * 60 - ) - out, warnings = process.wait_output() - if warnings: - for line in warnings.splitlines(): - logger.warning("CMD Out: %s", line.strip()) - logging.debug("Starting services in DB containers") - for ph in self.get_named_pebble_handlers(OVN_DB_CONTAINERS): - ph.start_service() - # Attempt to setup listers etc - self.configure_ovn() - self.start_northd() - self.check_pebble_handlers_ready() - - def configure_unit(self, event: ops.framework.EventBase) -> None: - """Run configuration on this unit.""" - self.check_leader_ready() - self.check_relation_handlers_ready(event) - self.open_ports() - self.init_container_services() - # Do not check_pebble_handlers_ready as northd is started later. - self._state.unit_bootstrapped = True - - def open_ports(self): - """Register ports in underlying cloud.""" - self.unit.open_port("tcp", 6641) - self.unit.open_port("tcp", 6642) - - def configure_ovn(self): - """Configure ovn listener.""" - inactivity_probe = ( - int(self.config["ovsdb-server-inactivity-probe"]) * 1000 - ) - self.configure_ovn_listener( - "nb", - { - self.ovsdb_cms.db_nb_port: { - "inactivity_probe": inactivity_probe, - }, - }, - ) - self.configure_ovn_listener( - "sb", - { - self.ovsdb_cms.db_sb_port: { - "inactivity_probe": inactivity_probe, - }, - }, - ) - self.configure_ovn_listener( - "sb", - { - self.ovsdb_cms.db_sb_admin_port: { - "inactivity_probe": inactivity_probe, - }, - }, - ) - - -if __name__ == "__main__": - main(OVNCentralOperatorCharm) diff --git a/src/ovn.py b/src/ovn.py deleted file mode 100644 index 153c618..0000000 --- a/src/ovn.py +++ /dev/null @@ -1,272 +0,0 @@ -# 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. - -"""OVN utilities.""" - -import os -import subprocess -import uuid - -import utils - -OVN_RUNDIR = "/var/run/ovn" -OVN_SYSCONFDIR = "/etc/ovn" - - -def ovn_appctl( - target, args, rundir=None, use_ovs_appctl=False, cmd_executor=None -): - """Run ovn/ovs-appctl for target with args and return output. - - :param target: Name of daemon to contact. Unless target begins with '/', - `ovn-appctl` looks for a pidfile and will build the path to - a /var/run/ovn/target.pid.ctl for you. - :type target: str - :param args: Command and arguments to pass to `ovn-appctl` - :type args: Tuple[str, ...] - :param rundir: Override path to sockets - :type rundir: Optional[str] - :param use_ovs_appctl: The ``ovn-appctl`` command appeared in OVN 20.03, - set this to True to use ``ovs-appctl`` instead. - :type use_ovs_appctl: bool - :returns: Output from command - :rtype: str - :raises: subprocess.CalledProcessError - """ - # NOTE(fnordahl): The ovsdb-server processes for the OVN databases use a - # non-standard naming scheme for their daemon control socket and we need - # to pass the full path to the socket. - if target in ( - "ovnnb_db", - "ovnsb_db", - ): - target = os.path.join(rundir or OVN_RUNDIR, target + ".ctl") - - if use_ovs_appctl: - tool = "ovs-appctl" - else: - tool = "ovn-appctl" - - if not cmd_executor: - cmd_executor = utils._run - return cmd_executor(tool, "-t", target, *args) - - -class OVNClusterStatus(object): - """Class for examining cluster status.""" - - def __init__( - self, - name, - cluster_id, - server_id, - address, - status, - role, - term, - leader, - vote, - election_timer, - log, - entries_not_yet_committed, - entries_not_yet_applied, - connections, - servers, - ): - """Initialize and populate OVNClusterStatus object. - - Use class initializer so we can define types in a compatible manner. - - :param name: Name of schema used for database - :type name: str - :param cluster_id: UUID of cluster - :type cluster_id: uuid.UUID - :param server_id: UUID of server - :type server_id: uuid.UUID - :param address: OVSDB connection method - :type address: str - :param status: Status text - :type status: str - :param role: Role of server - :type role: str - :param term: Election term - :type term: int - :param leader: Short form UUID of leader - :type leader: str - :param vote: Vote - :type vote: str - :param election_timer: Current value of election timer - :type election_timer: int - :param log: Log - :type log: str - :param entries_not_yet_committed: Entries not yet committed - :type entries_not_yet_committed: int - :param entries_not_yet_applied: Entries not yet applied - :type entries_not_yet_applied: int - :param connections: Connections - :type connections: str - :param servers: Servers in the cluster - [('0ea6', 'ssl:192.0.2.42:6643')] - :type servers: List[Tuple[str,str]] - """ - self.name = name - self.cluster_id = cluster_id - self.server_id = server_id - self.address = address - self.status = status - self.role = role - self.term = term - self.leader = leader - self.vote = vote - self.election_timer = election_timer - self.log = log - self.entries_not_yet_committed = entries_not_yet_committed - self.entries_not_yet_applied = entries_not_yet_applied - self.connections = connections - self.servers = servers - - def __eq__(self, other): - """Whether statuses are equal.""" - return ( - self.name == other.name - and self.cluster_id == other.cluster_id - and self.server_id == other.server_id - and self.address == other.address - and self.status == other.status - and self.role == other.role - and self.term == other.term - and self.leader == other.leader - and self.vote == other.vote - and self.election_timer == other.election_timer - and self.log == other.log - and self.entries_not_yet_committed - == other.entries_not_yet_committed - and self.entries_not_yet_applied == other.entries_not_yet_applied - and self.connections == other.connections - and self.servers == other.servers - ) - - @property - def is_cluster_leader(self): - """Retrieve status information from clustered OVSDB. - - :returns: Whether target is cluster leader - :rtype: bool - """ - return self.leader == "self" - - -def cluster_status( - target, schema=None, use_ovs_appctl=False, rundir=None, cmd_executor=None -): - """Retrieve status information from clustered OVSDB. - - :param target: Usually one of 'ovsdb-server', 'ovnnb_db', 'ovnsb_db', can - also be full path to control socket. - :type target: str - :param schema: Database schema name, deduced from target if not provided - :type schema: Optional[str] - :param use_ovs_appctl: The ``ovn-appctl`` command appeared in OVN 20.03, - set this to True to use ``ovs-appctl`` instead. - :type use_ovs_appctl: bool - :param rundir: Override path to sockets - :type rundir: Optional[str] - :returns: cluster status data object - :rtype: OVNClusterStatus - :raises: subprocess.CalledProcessError, KeyError, RuntimeError - """ - schema_map = { - "ovnnb_db": "OVN_Northbound", - "ovnsb_db": "OVN_Southbound", - } - if schema and schema not in schema_map.keys(): - raise RuntimeError('Unknown schema provided: "{}"'.format(schema)) - - status = {} - k = "" - for line in ovn_appctl( - target, - ("cluster/status", schema or schema_map[target]), - rundir=rundir, - use_ovs_appctl=use_ovs_appctl, - cmd_executor=cmd_executor, - ).splitlines(): - if k and line.startswith(" "): - # there is no key which means this is a instance of a multi-line/ - # multi-value item, populate the List which is already stored under - # the key. - if k == "servers": - status[k].append( - tuple(line.replace(")", "").lstrip().split()[0:4:3]) - ) - else: - status[k].append(line.lstrip()) - elif ":" in line: - # this is a line with a key - k, v = line.split(":", 1) - k = k.lower() - k = k.replace(" ", "_") - if v: - # this is a line with both key and value - if k in ( - "cluster_id", - "server_id", - ): - v = v.replace("(", "") - v = v.replace(")", "") - status[k] = tuple(v.split()) - else: - status[k] = v.lstrip() - else: - # this is a line with only key which means a multi-line/ - # multi-value item. Store key as List which will be - # populated on subsequent iterations. - status[k] = [] - return OVNClusterStatus( - status["name"], - uuid.UUID(status["cluster_id"][1]), - uuid.UUID(status["server_id"][1]), - status["address"], - status["status"], - status["role"], - int(status["term"]), - status["leader"], - status["vote"], - int(status["election_timer"]), - status["log"], - int(status["entries_not_yet_committed"]), - int(status["entries_not_yet_applied"]), - status["connections"], - status["servers"], - ) - - -def is_northd_active(cmd_executor=None): - """Query `ovn-northd` for active status. - - Note that the active status information for ovn-northd is available for - OVN 20.03 and onward. - - :returns: True if local `ovn-northd` instance is active, False otherwise - :rtype: bool - """ - try: - for line in ovn_appctl( - "ovn-northd", ("status",), cmd_executor=cmd_executor - ).splitlines(): - if line.startswith("Status:") and "active" in line: - return True - except subprocess.CalledProcessError: - pass - return False diff --git a/src/ovsdb.py b/src/ovsdb.py deleted file mode 100644 index e12d952..0000000 --- a/src/ovsdb.py +++ /dev/null @@ -1,275 +0,0 @@ -# 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. - -"""Interface for interacting with OVSDB.""" - -import json -import uuid - -import utils - - -class SimpleOVSDB(object): - """Simple interface to OVSDB through the use of command line tools. - - OVS and OVN is managed through a set of databases. These databases have - similar command line tools to manage them. We make use of the similarity - to provide a generic class that can be used to manage them. - - The OpenvSwitch project does provide a Python API, but on the surface it - appears to be a bit too involved for our simple use case. - - Examples: - sbdb = SimpleOVSDB('ovn-sbctl') - for chs in sbdb.chassis: - print(chs) - - ovsdb = SimpleOVSDB('ovs-vsctl') - for br in ovsdb.bridge: - if br['name'] == 'br-test': - ovsdb.bridge.set(br['uuid'], 'external_ids:charm', 'managed') - - WARNING: If a list type field only have one item `ovs-vsctl` will present - it as a single item. Since we do not know the schema we have no way of - knowing what fields should be de-serialized as lists so the caller has - to be careful of checking the type of values returned from this library. - """ - - # For validation we keep a complete map of currently known good tool and - # table combinations. This requires maintenance down the line whenever - # upstream adds things that downstream wants, and the cost of maintaining - # that will most likely be lower then the cost of finding the needle in - # the haystack whenever downstream code misspells something. - _tool_table_map = { - "ovs-vsctl": ( - "autoattach", - "bridge", - "ct_timeout_policy", - "ct_zone", - "controller", - "datapath", - "flow_sample_collector_set", - "flow_table", - "ipfix", - "interface", - "manager", - "mirror", - "netflow", - "open_vswitch", - "port", - "qos", - "queue", - "ssl", - "sflow", - ), - "ovn-nbctl": ( - "acl", - "address_set", - "connection", - "dhcp_options", - "dns", - "forwarding_group", - "gateway_chassis", - "ha_chassis", - "ha_chassis_group", - "load_balancer", - "load_balancer_health_check", - "logical_router", - "logical_router_policy", - "logical_router_port", - "logical_router_static_route", - "logical_switch", - "logical_switch_port", - "meter", - "meter_band", - "nat", - "nb_global", - "port_group", - "qos", - "ssl", - ), - "ovn-sbctl": ( - "address_set", - "chassis", - "connection", - "controller_event", - "dhcp_options", - "dhcpv6_options", - "dns", - "datapath_binding", - "encap", - "gateway_chassis", - "ha_chassis", - "ha_chassis_group", - "igmp_group", - "ip_multicast", - "logical_flow", - "mac_binding", - "meter", - "meter_band", - "multicast_group", - "port_binding", - "port_group", - "rbac_permission", - "rbac_role", - "sb_global", - "ssl", - "service_monitor", - ), - } - - def __init__(self, tool, args=None, cmd_executor=None): - """The SimpleOVSDB constructor. - - :param tool: Which tool with database commands to operate on. - Usually one of `ovs-vsctl`, `ovn-nbctl`, `ovn-sbctl` - :type tool: str - :param args: Extra arguments to pass to the tool - :type args: Optional[List[str]] - """ - if tool not in self._tool_table_map: - raise RuntimeError( - 'tool must be one of "{}"'.format(self._tool_table_map.keys()) - ) - self._tool = tool - self._args = args - self.cmd_executor = cmd_executor or utils._run - - def __getattr__(self, table): - """Get table for tool.""" - if table not in self._tool_table_map[self._tool]: - raise AttributeError( - 'table "{}" not known for use with "{}"'.format( - table, self._tool - ) - ) - return self.Table( - self._tool, table, args=self._args, cmd_executor=self.cmd_executor - ) - - class Table(object): - """Methods to interact with contents of OVSDB tables. - - NOTE: At the time of this writing ``find`` is the only command - line argument to OVSDB manipulating tools that actually supports - JSON output. - """ - - def __init__(self, tool, table, args=None, cmd_executor=None): - """Run SimpleOVSDBTable constructor. - - :param table: Which table to operate on - :type table: str - :param args: Extra arguments to pass to the tool - :type args: Optional[List[str]] - """ - self._tool = tool - self._table = table - self._args = args - self.cmd_executor = cmd_executor or utils._run - - def _deserialize_ovsdb(self, data): - """Deserialize OVSDB RFC7047 section 5.1 data. - - :param data: Multidimensional list where first row contains RFC7047 - type information - :type data: List[str,any] - :returns: Deserialized data. - :rtype: any - """ - # When using json formatted output to OVS commands Internal OVSDB - # notation may occur that require further deserializing. - # Reference: https://tools.ietf.org/html/rfc7047#section-5.1 - ovs_type_cb_map = { - "uuid": uuid.UUID, - # NOTE: OVSDB sets have overloaded type - # see special handling below - "set": list, - "map": dict, - } - assert len(data) > 1, ( - "Invalid data provided, expecting list " - "with at least two elements." - ) - if data[0] == "set": - # special handling for set - # - # it is either a list of strings or a list of typed lists. - # taste first element to see which it is - for el in data[1]: - # NOTE: We lock this handling down to the `uuid` type as - # that is the only one we have a practical example of. - # We could potentially just handle this generally based on - # the types listed in `ovs_type_cb_map` but let's open for - # that as soon as we have a concrete example to validate on - if isinstance(el, list) and len(el) and el[0] == "uuid": - decoded_set = [] - for el in data[1]: - decoded_set.append(self._deserialize_ovsdb(el)) - return decoded_set - # fall back to normal processing below - break - - # Use map to deserialize data with fallback to `str` - f = ovs_type_cb_map.get(data[0], str) - return f(data[1]) - - def _find_tbl(self, condition=None): - """Run and parse output of OVSDB `find` command. - - :param condition: An optional RFC 7047 5.1 match condition - :type condition: Optional[str] - :returns: Dictionary with data - :rtype: Dict[str, any] - """ - cmd = [self._tool] - if self._args: - cmd.extend(self._args) - cmd.extend(["-f", "json", "find", self._table]) - if condition: - cmd.append(condition) - output = self.cmd_executor(*cmd) - data = json.loads(output) - for row in data["data"]: - values = [] - for col in row: - if isinstance(col, list) and len(col) > 1: - values.append(self._deserialize_ovsdb(col)) - else: - values.append(col) - yield dict(zip(data["headings"], values)) - - def __iter__(self): - """Iterate over values in OVSDB table.""" - return self._find_tbl() - - def clear(self, rec, col): - """Clear value from OVSDB table.""" - self.cmd_executor(self._tool, "clear", self._table, rec, col) - - def find(self, condition): - """Find value in OVSDB table.""" - return self._find_tbl(condition=condition) - - def remove(self, rec, col, value): - """Remove value from OVSDB table.""" - self.cmd_executor( - self._tool, "remove", self._table, rec, col, value - ) - - def set(self, rec, col, value): - """Set value in OVSDB table.""" - self.cmd_executor( - self._tool, "set", self._table, rec, "{}={}".format(col, value) - ) diff --git a/src/templates/cert_host.j2 b/src/templates/cert_host.j2 deleted file mode 100644 index 0d92c10..0000000 --- a/src/templates/cert_host.j2 +++ /dev/null @@ -1,2 +0,0 @@ -# {{ certificates }} -{{ certificates.cert }} diff --git a/src/templates/key_host.j2 b/src/templates/key_host.j2 deleted file mode 100644 index 63a9aeb..0000000 --- a/src/templates/key_host.j2 +++ /dev/null @@ -1 +0,0 @@ -{{ certificates.key }} diff --git a/src/templates/ovn-central.crt.j2 b/src/templates/ovn-central.crt.j2 deleted file mode 100644 index 00cd0da..0000000 --- a/src/templates/ovn-central.crt.j2 +++ /dev/null @@ -1 +0,0 @@ -{{ certificates.ca_cert }} diff --git a/src/templates/ovn-nb-cluster-join.sh.j2 b/src/templates/ovn-nb-cluster-join.sh.j2 deleted file mode 100644 index 2fed32a..0000000 --- a/src/templates/ovn-nb-cluster-join.sh.j2 +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/bash - -WRAPPER_LOG=/var/log/ovn/cluster_join_wrapper.log - -function log_msg() -{ - msg=$1 - echo "$(date): $msg" >> $WRAPPER_LOG -} - -ls -l /var/lib/ovn/{.ovn,ovn}nb* &> /dev/null && { log_msg "Existing DB files found, skipping join"; exit 0; } -log_msg "No existing DB files present" -ADDRESSES="{{ peers.db_nb_cluster_connection_strs|join(' ') }}" -log_msg "Running join-cluster cmd: ovsdb-tool join-cluster /var/lib/ovn/ovnnb_db.db OVN_Northbound $ADDRESSES" -ovsdb-tool join-cluster /var/lib/ovn/ovnnb_db.db OVN_Northbound $ADDRESSES &> $WRAPPER_LOG - diff --git a/src/templates/ovn-nb-db-server-wrapper.sh.j2 b/src/templates/ovn-nb-db-server-wrapper.sh.j2 deleted file mode 100644 index 0366505..0000000 --- a/src/templates/ovn-nb-db-server-wrapper.sh.j2 +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/bash - -function log_msg() -{ - msg=$1 - echo "$(date) [wrapper]: $msg" -} - -function stop_svc() -{ - log_msg "Stopping" - /usr/share/ovn/scripts/ovn-ctl stop_nb_ovsdb 2>&1 -} - -trap stop_svc EXIT - -log_msg "Starting" - -/usr/share/ovn/scripts/ovn-ctl run_nb_ovsdb \ - --db-nb-cluster-local-addr={{ peers.cluster_local_hostname }} \ - --db-nb-cluster-local-port={{ peers.db_nb_cluster_port }} \ - --db-nb-cluster-local-proto=ssl \ - --db-nb-cluster-remote-addr={{ peers.cluster_remote_hostnames | first if not ovs_db.is_charm_leader else '' }} \ - --db-nb-cluster-remote-port={{ peers.db_nb_cluster_port }} \ - --db-nb-cluster-remote-proto=ssl \ - --ovn-nb-db-ssl-key={{ ovs_db.ovn_key }} \ - --ovn-nb-db-ssl-cert={{ ovs_db.ovn_cert }} \ - --ovn-nb-db-ssl-ca-cert={{ ovs_db.ovn_ca_cert }} \ - --ovn-nb-log="-vconsole:info" diff --git a/src/templates/ovn-northd-db-params.conf.j2 b/src/templates/ovn-northd-db-params.conf.j2 deleted file mode 100644 index 1855d7a..0000000 --- a/src/templates/ovn-northd-db-params.conf.j2 +++ /dev/null @@ -1,5 +0,0 @@ ---ovnnb-db={{ peers.db_nb_connection_strs|join(',') }} ---ovnsb-db={{ peers.db_sb_connection_strs|join(',') }} --c {{ ovs_db.ovn_cert }} --C {{ ovs_db.ovn_ca_cert }} --p {{ ovs_db.ovn_key }} diff --git a/src/templates/ovn-northd-wrapper.sh.j2 b/src/templates/ovn-northd-wrapper.sh.j2 deleted file mode 100644 index 1a02520..0000000 --- a/src/templates/ovn-northd-wrapper.sh.j2 +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/bash - -function log_msg() -{ - msg=$1 - echo "$(date) [wrapper]: $msg" -} - -function stop_svc() -{ - log_msg "Stopping" - /usr/share/ovn/scripts/ovn-ctl stop_northd 2>&1 -} - -trap stop_svc EXIT - -log_msg "Starting" - -# Switch to using ovn-ctl if its possible to stop --detach -# being set - -ovn-northd \ - -vconsole:info \ - --ovnnb-db={{ peers.db_nb_connection_strs|join(',') }} \ - --ovnsb-db={{ peers.db_sb_connection_strs|join(',') }} \ - -c {{ ovs_db.ovn_cert }} \ - -C {{ ovs_db.ovn_ca_cert }} \ - -p {{ ovs_db.ovn_key }} \ - --no-chdir \ - --pidfile=/var/run/ovn/ovn-northd.pid diff --git a/src/templates/ovn-sb-cluster-join.sh.j2 b/src/templates/ovn-sb-cluster-join.sh.j2 deleted file mode 100644 index 434dff9..0000000 --- a/src/templates/ovn-sb-cluster-join.sh.j2 +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/bash - -WRAPPER_LOG=/var/log/ovn/cluster_join_wrapper.log - -function log_msg() -{ - msg=$1 - echo "$(date): $msg" >> $WRAPPER_LOG -} -ls -l /var/lib/ovn/{.ovn,ovn}sb* &> /dev/null && { log_msg "Existing DB files found, skipping join"; exit 0; } -log_msg "No existing DB files present" -ADDRESSES="{{ peers.db_sb_cluster_connection_strs|join(' ') }}" -log_msg "Running join-cluster cmd: ovsdb-tool join-cluster /var/lib/ovn/ovnsb_db.db OVN_Southbound $ADDRESSES" -# ovsdb-tool join-cluster /var/lib/ovn/ovnsb_db.db OVN_Southbound ssl\:10.1.8.116\:6644 ssl\:10.1.46.82\:6644 ssl\:10.1.41.7\:6644 -ovsdb-tool join-cluster /var/lib/ovn/ovnsb_db.db OVN_Southbound $ADDRESSES &> $WRAPPER_LOG diff --git a/src/templates/ovn-sb-db-server-wrapper.sh.j2 b/src/templates/ovn-sb-db-server-wrapper.sh.j2 deleted file mode 100644 index 49e7dfe..0000000 --- a/src/templates/ovn-sb-db-server-wrapper.sh.j2 +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/bash - -function log_msg() -{ - msg=$1 - echo "$(date) [wrapper]: $msg" -} - -function stop_svc() -{ - log_msg "Stopping" - /usr/share/ovn/scripts/ovn-ctl stop_sb_ovsdb 2>&1 -} - -trap stop_svc EXIT - -log_msg "Starting" - -/usr/share/ovn/scripts/ovn-ctl run_sb_ovsdb \ - --db-sb-cluster-local-addr={{ peers.cluster_local_hostname }} \ - --db-sb-cluster-local-port={{ peers.db_sb_cluster_port }} \ - --db-sb-cluster-local-proto=ssl \ - --db-sb-cluster-remote-addr={{ peers.cluster_remote_hostnames | first if not ovs_db.is_charm_leader else '' }} \ - --db-sb-cluster-remote-port={{ peers.db_sb_cluster_port }} \ - --db-sb-cluster-remote-proto=ssl \ - --ovn-sb-db-ssl-key={{ ovs_db.ovn_key }} \ - --ovn-sb-db-ssl-cert={{ ovs_db.ovn_cert }} \ - --ovn-sb-db-ssl-ca-cert={{ ovs_db.ovn_ca_cert }} \ - --ovn-sb-log="-vconsole:info" diff --git a/src/utils.py b/src/utils.py deleted file mode 100644 index ca52288..0000000 --- a/src/utils.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2019 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. - -"""Patch utilities.""" - -import subprocess - - -def _run(*args): - """Run a process, check result, capture decoded output from STDOUT. - - :param args: Command and arguments to run - :type args: Tuple[str, ...] - :returns: Information about the completed process - :rtype: str - :raises subprocess.CalledProcessError - """ - return subprocess.check_output(args, universal_newlines=True) diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index da3b04b..0000000 --- a/test-requirements.txt +++ /dev/null @@ -1,12 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -coverage -mock -stestr -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/tests/bundles/smoke.yaml b/tests/bundles/smoke.yaml deleted file mode 100644 index a5b0f3d..0000000 --- a/tests/bundles/smoke.yaml +++ /dev/null @@ -1,133 +0,0 @@ -bundle: kubernetes - -applications: - traefik: - charm: ch:traefik-k8s - channel: 1.0/candidate - scale: 1 - trust: true - traefik-public: - charm: ch:traefik-k8s - channel: 1.0/candidate - scale: 1 - trust: true - options: - kubernetes-service-annotations: metallb.universe.tf/address-pool=public - mysql: - charm: ch:mysql-k8s - channel: 8.0/stable - scale: 1 - trust: false - rabbitmq: - charm: ch:rabbitmq-k8s - channel: 3.12/edge - scale: 1 - trust: true - keystone: - charm: ch:keystone-k8s - channel: 2023.2/edge - scale: 1 - trust: true - options: - admin-role: admin - storage: - fernet-keys: 5M - credential-keys: 5M - glance: - charm: ch:glance-k8s - channel: 2023.2/edge - scale: 1 - trust: true - storage: - local-repository: 5G - nova: - charm: ch:nova-k8s - channel: 2023.2/edge - scale: 1 - trust: true - placement: - charm: ch:placement-k8s - channel: 2023.2/edge - scale: 1 - trust: true - neutron: - charm: ch:neutron-k8s - channel: 2023.2/edge - scale: 1 - trust: true - ovn-central: - charm: ch:ovn-central-k8s - charm: ../../ovn-central-k8s.charm - scale: 1 - trust: true - resources: - ovn-sb-db-server-image: ghcr.io/canonical/ovn-consolidated:23.09 - ovn-nb-db-server-image: ghcr.io/canonical/ovn-consolidated:23.09 - ovn-northd-image: ghcr.io/canonical/ovn-consolidated:23.09 - tls-operator: - charm: self-signed-certificates - channel: edge - scale: 1 - options: - ca-common-name: internal-ca - -relations: -- - mysql:database - - keystone:database -- - traefik:ingress - - keystone:ingress-internal -- - traefik-public:ingress - - keystone:ingress-public - -- - mysql:database - - glance:database -- - rabbitmq:amqp - - glance:amqp -- - keystone:identity-service - - glance:identity-service -- - traefik:ingress - - glance:ingress-internal -- - traefik-public:ingress - - glance:ingress-public - -- - mysql:database - - nova:database -- - mysql:database - - nova:api-database -- - mysql:database - - nova:cell-database -- - rabbitmq:amqp - - nova:amqp -- - keystone:identity-service - - nova:identity-service -- - traefik:ingress - - nova:ingress-internal -- - traefik-public:ingress - - nova:ingress-public - -- - mysql:database - - placement:database -- - keystone:identity-service - - placement:identity-service -- - traefik:ingress - - placement:ingress-internal -- - traefik-public:ingress - - placement:ingress-public - -- - mysql:database - - neutron:database -- - rabbitmq:amqp - - neutron:amqp -- - keystone:identity-service - - neutron:identity-service -- - traefik:ingress - - neutron:ingress-internal -- - traefik-public:ingress - - neutron:ingress-public -- - tls-operator:certificates - - neutron:certificates -- - neutron:ovsdb-cms - - ovn-central:ovsdb-cms - -- - tls-operator:certificates - - ovn-central:certificates diff --git a/tests/config.yaml b/tests/config.yaml deleted file mode 120000 index e84e89a..0000000 --- a/tests/config.yaml +++ /dev/null @@ -1 +0,0 @@ -../config.yaml \ No newline at end of file diff --git a/tests/tests.yaml b/tests/tests.yaml deleted file mode 100644 index a0b7a76..0000000 --- a/tests/tests.yaml +++ /dev/null @@ -1,70 +0,0 @@ -gate_bundles: - - smoke -smoke_bundles: - - smoke -configure: - - zaza.openstack.charm_tests.keystone.setup.wait_for_all_endpoints - - zaza.openstack.charm_tests.keystone.setup.add_tempest_roles - - zaza.openstack.charm_tests.nova.setup.create_flavors - - zaza.openstack.charm_tests.nova.setup.manage_ssh_key -# - zaza.openstack.charm_tests.glance.setup.add_lts_image -# - zaza.openstack.charm_tests.glance.setup.add_cirros_image -# - zaza.openstack.charm_tests.glance.setup.add_cirros_alt_image -tests: - - zaza.openstack.charm_tests.tempest.tests.TempestTestWithKeystoneMinimal -tests_options: - trust: - - smoke - ignore_hard_deploy_errors: - - smoke - - tempest: - default: - smoke: True - exclude-list: - - "tempest.api.image.v2.test_images.BasicOperationsImagesTest.test_register_upload_get_image_file" - - "tempest.api.compute.security_groups.test_security_group_rules.SecurityGroupRulesTestJSON.test_security_group_rules_create" - - "tempest.api.compute.security_groups.test_security_group_rules.SecurityGroupRulesTestJSON.test_security_group_rules_list" - - "tempest.api.compute.security_groups.test_security_groups.SecurityGroupsTestJSON.test_security_groups_create_list_delete" - - "tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON" - - "tempest.api.compute.servers.test_create_server.ServersTestManualDisk" - - "tempest.api.compute.servers.test_server_addresses.ServerAddressesTestJSON" - - "tempest.api.compute.servers.test_create_server.ServersTestJSON" - - "tempest.scenario.test_server_multinode.TestServerMultinode.test_schedule_to_all_nodes" - - "tempest.scenario.test_server_basic_ops.TestServerBasicOps.test_server_basic_ops" - - "tempest.api.compute.servers.test_attach_interfaces.AttachInterfacesUnderV243Test.test_add_remove_fixed_ip" - -target_deploy_status: - traefik: - workload-status: active - workload-status-message-regex: '^$' - traefik-public: - workload-status: active - workload-status-message-regex: '^$' - rabbitmq: - workload-status: active - workload-status-message-regex: '^$' - nova: - workload-status: active - workload-status-message-regex: '^$' - glance: - workload-status: active - workload-status-message-regex: '^$' - neutron: - workload-status: active - workload-status-message-regex: '^$' - ovn-central: - workload-status: active - workload-status-message-regex: '^$' - keystone: - workload-status: active - workload-status-message-regex: '^$' - mysql: - workload-status: active - workload-status-message-regex: '^.*$' - placement: - workload-status: active - workload-status-message-regex: '^$' - tls-operator: - workload-status: active - workload-status-message-regex: '^$' diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py deleted file mode 100644 index 304f420..0000000 --- a/tests/unit/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# 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 charm.""" diff --git a/tests/unit/test_ovn_central_charm.py b/tests/unit/test_ovn_central_charm.py deleted file mode 100644 index 2b4e5c0..0000000 --- a/tests/unit/test_ovn_central_charm.py +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2021 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. - -"""Tests for OVN central charm.""" - -import mock -import ops_sunbeam.test_utils as test_utils - -import charm - - -class _OVNCentralOperatorCharm(charm.OVNCentralOperatorCharm): - def __init__(self, framework): - self.seen_events = [] - super().__init__(framework) - - def _log_event(self, event): - self.seen_events.append(type(event).__name__) - - def configure_charm(self, event): - super().configure_charm(event) - self._log_event(event) - - def configure_ovn_listener(self, db, port_map): - pass - - def cluster_status(self, db, cmd_executor): - if db == "ovnnb_db": - nb_mock = mock.MagicMock() - nb_mock.cluster_id = "nb_id" - return nb_mock - if db == "ovnsb_db": - sb_mock = mock.MagicMock() - sb_mock.cluster_id = "sb_id" - return sb_mock - - -class TestOVNCentralOperatorCharm(test_utils.CharmTestCase): - """Class for testing OVN central charm.""" - - PATCHES = [] - - def setUp(self): - """Setup Glance tests.""" - super().setUp(charm, self.PATCHES) - self.harness = test_utils.get_harness( - _OVNCentralOperatorCharm, container_calls=self.container_calls - ) - self.addCleanup(self.harness.cleanup) - self.harness.begin() - - def test_pebble_ready_handler(self): - """Test Pebble ready event is captured.""" - self.assertEqual(self.harness.charm.seen_events, []) - test_utils.set_all_pebbles_ready(self.harness) - self.assertEqual(len(self.harness.charm.seen_events), 3) - - def check_rendered_files(self): - """Check all files are rendered.""" - sb_config_files = [ - "/etc/ovn/cert_host", - "/etc/ovn/key_host", - "/etc/ovn/ovn-central.crt", - "/root/ovn-sb-cluster-join.sh", - "/root/ovn-sb-db-server-wrapper.sh", - ] - for f in sb_config_files: - self.check_file("ovn-sb-db-server", f) - - nb_config_files = [ - "/etc/ovn/cert_host", - "/etc/ovn/key_host", - "/etc/ovn/ovn-central.crt", - "/root/ovn-nb-cluster-join.sh", - "/root/ovn-nb-db-server-wrapper.sh", - ] - for f in nb_config_files: - self.check_file("ovn-nb-db-server", f) - - northd_config_files = [ - "/etc/ovn/cert_host", - "/etc/ovn/key_host", - "/etc/ovn/ovn-central.crt", - "/etc/ovn/ovn-northd-db-params.conf", - "/root/ovn-northd-wrapper.sh", - ] - for f in northd_config_files: - self.check_file("ovn-northd", f) - - def test_all_relations_leader(self): - """Test all the charms relations.""" - self.harness.set_leader() - self.assertEqual(self.harness.charm.seen_events, []) - test_utils.set_all_pebbles_ready(self.harness) - test_utils.add_all_relations(self.harness) - self.check_rendered_files() - - def test_all_relations_non_leader(self): - """Test all the charms relations on non-leader.""" - self.harness.set_leader(False) - self.assertEqual(self.harness.charm.seen_events, []) - test_utils.set_all_pebbles_ready(self.harness) - rel_ids = test_utils.add_all_relations(self.harness) - test_utils.set_remote_leader_ready(self.harness, rel_ids["peers"]) - self.harness.update_relation_data( - rel_ids["peers"], - self.harness.charm.app.name, - {"nb_cid": "nbcid", "sb_cid": "sbcid"}, - ) - self.harness.update_relation_data( - rel_ids["peers"], - f"{self.harness.charm.app.name}/1", - {"bound-hostname": "ovn-central-1"}, - ) - self.check_rendered_files() - self.assertEqual( - self.container_calls.execute["ovn-sb-db-server"], - [["bash", "/root/ovn-sb-cluster-join.sh"]], - ) - self.assertEqual( - self.container_calls.execute["ovn-nb-db-server"], - [["bash", "/root/ovn-nb-cluster-join.sh"]], - ) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 2b8f98b..0000000 --- a/tox.ini +++ /dev/null @@ -1,169 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - PYTHONPATH - HOME -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -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 - -[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<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504