From 9bbd69a8c6f8bd2f3c637e891ea9d16ffa432b7f Mon Sep 17 00:00:00 2001 From: James Page Date: Tue, 12 Dec 2023 10:59:11 +0000 Subject: [PATCH] Retire project post merge to sunbeam-charms Change-Id: I88925ea8818f28b59137105ac1f268a1ebaf9e81 --- .flake8 | 9 - .jujuignore | 3 - .stestr.conf | 3 - .zuul.yaml | 10 +- CONTRIBUTING.md | 55 - LICENSE | 202 --- README.md | 70 - README.rst | 12 + actions.yaml | 2 - charmcraft.yaml | 32 - config.yaml | 29 - fetch-libs.sh | 4 - lib/charms/ovn_central_k8s/v0/ovsdb.py | 206 --- .../v1/tls_certificates.py | 1261 ----------------- metadata.yaml | 83 -- osci.yaml | 10 - pyproject.toml | 39 - rename.sh | 13 - requirements.txt | 14 - src/charm.py | 498 ------- src/ovn.py | 272 ---- src/ovsdb.py | 275 ---- src/templates/cert_host.j2 | 2 - src/templates/key_host.j2 | 1 - src/templates/ovn-central.crt.j2 | 1 - src/templates/ovn-nb-cluster-join.sh.j2 | 16 - src/templates/ovn-nb-db-server-wrapper.sh.j2 | 29 - src/templates/ovn-northd-db-params.conf.j2 | 5 - src/templates/ovn-northd-wrapper.sh.j2 | 30 - src/templates/ovn-sb-cluster-join.sh.j2 | 15 - src/templates/ovn-sb-db-server-wrapper.sh.j2 | 29 - src/utils.py | 29 - test-requirements.txt | 12 - tests/bundles/smoke.yaml | 133 -- tests/config.yaml | 1 - tests/tests.yaml | 70 - tests/unit/__init__.py | 15 - tests/unit/test_ovn_central_charm.py | 136 -- tox.ini | 169 --- 39 files changed, 13 insertions(+), 3782 deletions(-) delete mode 100644 .flake8 delete mode 100644 .jujuignore delete mode 100644 .stestr.conf delete mode 100644 CONTRIBUTING.md delete mode 100644 LICENSE delete mode 100644 README.md create mode 100644 README.rst delete mode 100644 actions.yaml delete mode 100644 charmcraft.yaml delete mode 100644 config.yaml delete mode 100755 fetch-libs.sh delete mode 100644 lib/charms/ovn_central_k8s/v0/ovsdb.py delete mode 100644 lib/charms/tls_certificates_interface/v1/tls_certificates.py delete mode 100644 metadata.yaml delete mode 100644 osci.yaml delete mode 100644 pyproject.toml delete mode 100755 rename.sh delete mode 100644 requirements.txt delete mode 100755 src/charm.py delete mode 100644 src/ovn.py delete mode 100644 src/ovsdb.py delete mode 100644 src/templates/cert_host.j2 delete mode 100644 src/templates/key_host.j2 delete mode 100644 src/templates/ovn-central.crt.j2 delete mode 100644 src/templates/ovn-nb-cluster-join.sh.j2 delete mode 100644 src/templates/ovn-nb-db-server-wrapper.sh.j2 delete mode 100644 src/templates/ovn-northd-db-params.conf.j2 delete mode 100644 src/templates/ovn-northd-wrapper.sh.j2 delete mode 100644 src/templates/ovn-sb-cluster-join.sh.j2 delete mode 100644 src/templates/ovn-sb-db-server-wrapper.sh.j2 delete mode 100644 src/utils.py delete mode 100644 test-requirements.txt delete mode 100644 tests/bundles/smoke.yaml delete mode 120000 tests/config.yaml delete mode 100644 tests/tests.yaml delete mode 100644 tests/unit/__init__.py delete mode 100644 tests/unit/test_ovn_central_charm.py delete mode 100644 tox.ini 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