From: Mark Beierl Date: Fri, 12 Aug 2022 15:13:24 +0000 (-0400) Subject: Add VCA Integrator Charm X-Git-Tag: release-v13.0-start~49 X-Git-Url: https://osm.etsi.org/gitweb/?a=commitdiff_plain;h=refs%2Fchanges%2F56%2F12456%2F2;p=osm%2Fdevops.git Add VCA Integrator Charm Change-Id: Ic42bb4f8b682678d3e49aa671e2de9661a84af6c Signed-off-by: Mark Beierl --- diff --git a/installers/charm/vca-integrator-operator/.gitignore b/installers/charm/vca-integrator-operator/.gitignore new file mode 100644 index 00000000..9ac35bd1 --- /dev/null +++ b/installers/charm/vca-integrator-operator/.gitignore @@ -0,0 +1,25 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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. +####################################################################################### +venv/ +build/ +*.charm +.tox/ +.coverage +coverage.xml +__pycache__/ +*.py[cod] +.vscode diff --git a/installers/charm/vca-integrator-operator/.jujuignore b/installers/charm/vca-integrator-operator/.jujuignore new file mode 100644 index 00000000..5cee0249 --- /dev/null +++ b/installers/charm/vca-integrator-operator/.jujuignore @@ -0,0 +1,20 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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. +####################################################################################### + +/venv +*.py[cod] +*.charm diff --git a/installers/charm/vca-integrator-operator/CONTRIBUTING.md b/installers/charm/vca-integrator-operator/CONTRIBUTING.md new file mode 100644 index 00000000..94fc3039 --- /dev/null +++ b/installers/charm/vca-integrator-operator/CONTRIBUTING.md @@ -0,0 +1,74 @@ + + + +# Contributing + +## Overview + +This documents explains the processes and practices recommended for contributing enhancements to +the OSM VCA Integrator charm. + +- If you would like to chat with us about your use-cases or proposed implementation, you can reach + us at [Canonical Mattermost public channel](https://chat.charmhub.io/charmhub/channels/charm-dev) + or [Discourse](https://discourse.charmhub.io/). +- Familiarising yourself with the [Charmed Operator Framework](https://juju.is/docs/sdk) library + will help you a lot when working on new features or bug fixes. +- All enhancements require review before being merged. Code review typically examines + - code quality + - test coverage + - user experience for Juju administrators this charm. +- Please help us out in ensuring easy to review branches by rebasing your pull request branch onto + the `main` branch. This also avoids merge commits and creates a linear Git commit history. + +## Developing + +You can use the environments created by `tox` for development: + +```shell +tox --notest -e unit +source .tox/unit/bin/activate +``` + +### Testing + +```shell +tox -e fmt # update your code according to linting rules +tox -e lint # code style +tox -e unit # unit tests +tox -e integration # integration tests +tox # runs 'lint' and 'unit' environments +``` + +## Build charm + +Build the charm in this git repository using: + +```shell +charmcraft pack +``` + +### Deploy + +```bash +# Create a model +juju add-model test-osm-vca-integrator +# Enable DEBUG logging +juju model-config logging-config="=INFO;unit=DEBUG" +# Deploy the charm +juju deploy ./osm-vca-integrator_ubuntu-20.04-amd64.charm +``` + diff --git a/installers/charm/vca-integrator-operator/LICENSE b/installers/charm/vca-integrator-operator/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/installers/charm/vca-integrator-operator/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/installers/charm/vca-integrator-operator/README.md b/installers/charm/vca-integrator-operator/README.md new file mode 100644 index 00000000..140af91a --- /dev/null +++ b/installers/charm/vca-integrator-operator/README.md @@ -0,0 +1,43 @@ + + +# OSM VCA Integrator Operator + +## Description + +TODO + +## How-to guides + +### Deploy and configure + +Deploy the OSM VCA Integrator Charm using the Juju command line: + +```shell +$ juju add-model osm-vca-integrator +$ juju deploy osm-vca-integrator +$ juju config osm-vca-integrator \ + k8s-cloud=microk8s \ + controllers="`cat ~/.local/share/juju/controllers.yaml`" \ + accounts="`cat ~/.local/share/juju/accounts.yaml`" \ + public-key="`cat ~/.local/share/juju/ssh/juju_id_rsa.pub`" +``` + +## 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` for developer guidance. diff --git a/installers/charm/vca-integrator-operator/actions.yaml b/installers/charm/vca-integrator-operator/actions.yaml new file mode 100644 index 00000000..65d82b91 --- /dev/null +++ b/installers/charm/vca-integrator-operator/actions.yaml @@ -0,0 +1,16 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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. +####################################################################################### \ No newline at end of file diff --git a/installers/charm/vca-integrator-operator/charmcraft.yaml b/installers/charm/vca-integrator-operator/charmcraft.yaml new file mode 100644 index 00000000..0d7d5ebc --- /dev/null +++ b/installers/charm/vca-integrator-operator/charmcraft.yaml @@ -0,0 +1,35 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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. +####################################################################################### + +type: "charm" +bases: + - build-on: + - name: "ubuntu" + channel: "20.04" + run-on: + - name: "ubuntu" + channel: "20.04" +parts: + charm: + build-environment: + - CRYPTOGRAPHY_DONT_BUILD_RUST: 1 + build-packages: + - build-essential + - libssl-dev + - libffi-dev + - python3-dev + - cargo diff --git a/installers/charm/vca-integrator-operator/config.yaml b/installers/charm/vca-integrator-operator/config.yaml new file mode 100644 index 00000000..97b36cbd --- /dev/null +++ b/installers/charm/vca-integrator-operator/config.yaml @@ -0,0 +1,116 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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. +####################################################################################### + +options: + accounts: + description: | + Content of the .local/share/juju/accounts.yaml file, + which includes the relevant information about the accounts. + type: string + controllers: + description: | + Content of the .local/share/juju/controllers.yaml file, + which includes the relevant information about the controllers. + type: string + public-key: + description: | + Juju public key, usually located at ~/.local/share/juju/ssh/juju_id_rsa.pub + type: string + lxd-cloud: + description: | + Name and credentials of the lxd cloud. + This cloud will be used by N2VC to deploy LXD Proxy Charms. + + The expected input is the following: + [:] + + By default, the will be the same as + . + type: string + k8s-cloud: + description: | + Name and credentials of the k8s cloud. + This cloud will be used by N2VC to deploy K8s Proxy Charms. + + The expected input is the following: + [:] + + By default, the will be the same as + . + type: string + model-configs: + type: string + description: | + Yaml content with all the default model-configs to be sent + in the relation vca relation. + + Example: + juju config vca-integrator model-configs=' + agent-metadata-url: <> + agent-stream: ... + apt-ftp-proxy: + apt-http-proxy: + apt-https-proxy: + apt-mirror: + apt-no-proxy: + automatically-retry-hooks: + backup-dir: + cloudinit-userdata: + container-image-metadata-url: + container-image-stream: + container-inherit-properties: + container-networking-method: + default-series: + default-space: + development: + disable-network-management: + egress-subnets: + enable-os-refresh-update: + enable-os-upgrade: + fan-config: + firewall-mode: + ftp-proxy: + http-proxy: + https-proxy: + ignore-machine-addresses: + image-metadata-url: + image-stream: + juju-ftp-proxy: + juju-http-proxy: + juju-https-proxy: + juju-no-proxy: + logforward-enabled: + logging-config: + lxd-snap-channel: + max-action-results-age: + max-action-results-size: + max-status-history-age: + max-status-history-size: + net-bond-reconfigure-delay: + no-proxy: + provisioner-harvest-mode: + proxy-ssh: + snap-http-proxy: + snap-https-proxy: + snap-store-assertions: + snap-store-proxy: + snap-store-proxy-url: + ssl-hostname-verification: + test-mode: + transmit-vendor-metrics: + update-status-hook-interval: + ' diff --git a/installers/charm/vca-integrator-operator/lib/charms/osm_vca_integrator/v0/vca.py b/installers/charm/vca-integrator-operator/lib/charms/osm_vca_integrator/v0/vca.py new file mode 100644 index 00000000..21dac69c --- /dev/null +++ b/installers/charm/vca-integrator-operator/lib/charms/osm_vca_integrator/v0/vca.py @@ -0,0 +1,221 @@ +# Copyright 2022 Canonical Ltd. +# See LICENSE file for licensing details. +# +# 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. + +"""VCA Library. + +VCA stands for VNF Configuration and Abstraction, and is one of the core components +of OSM. The Juju Controller is in charged of this role. + +This [library](https://juju.is/docs/sdk/libraries) implements both sides of the +`vca` [interface](https://juju.is/docs/sdk/relations). + +The *provider* side of this interface is implemented by the +[osm-vca-integrator Charmed Operator](https://charmhub.io/osm-vca-integrator). + +helps to integrate with the +vca-integrator charm, which provides data needed to the OSM components that need +to talk to the VCA, and + +Any Charmed OSM component that *requires* to talk to the VCA should implement +the *requirer* side of this interface. + +In a nutshell using this library to implement a Charmed Operator *requiring* VCA data +would look like + +``` +$ charmcraft fetch-lib charms.osm_vca_integrator.v0.vca +``` + +`metadata.yaml`: + +``` +requires: + vca: + interface: osm-vca +``` + +`src/charm.py`: + +``` +from charms.osm_vca_integrator.v0.vca import VcaData, VcaIntegratorEvents, VcaRequires +from ops.charm import CharmBase + + +class MyCharm(CharmBase): + + on = VcaIntegratorEvents() + + def __init__(self, *args): + super().__init__(*args) + self.vca = VcaRequires(self) + self.framework.observe( + self.on.vca_data_changed, + self._on_vca_data_changed, + ) + + def _on_vca_data_changed(self, event): + # Get Vca data + data: VcaData = self.vca.data + # data.endpoints => "localhost:17070" +``` + +You can file bugs +[here](https://github.com/charmed-osm/osm-vca-integrator-operator/issues)! +""" + +import json +import logging +from typing import Any, Dict, Optional + +from ops.charm import CharmBase, CharmEvents, RelationChangedEvent +from ops.framework import EventBase, EventSource, Object + +# The unique Charmhub library identifier, never change it +from ops.model import Relation + +# The unique Charmhub library identifier, never change it +LIBID = "746b36c382984e5c8660b78192d84ef9" + +# 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 + + +logger = logging.getLogger(__name__) + + +class VcaDataChangedEvent(EventBase): + """Event emitted whenever there is a change in the vca data.""" + + def __init__(self, handle): + super().__init__(handle) + + +class VcaIntegratorEvents(CharmEvents): + """VCA Integrator events. + + This class defines the events that ZooKeeper can emit. + + Events: + vca_data_changed (_VcaDataChanged) + """ + + vca_data_changed = EventSource(VcaDataChangedEvent) + + +RELATION_MANDATORY_KEYS = ("endpoints", "user", "secret", "public-key", "cacert", "model-configs") + + +class VcaData: + """Vca data class.""" + + def __init__(self, data: Dict[str, Any]) -> None: + self.data: str = data + self.endpoints: str = data["endpoints"] + self.user: str = data["user"] + self.secret: str = data["secret"] + self.public_key: str = data["public-key"] + self.cacert: str = data["cacert"] + self.lxd_cloud: str = data.get("lxd-cloud") + self.lxd_credentials: str = data.get("lxd-credentials") + self.k8s_cloud: str = data.get("k8s-cloud") + self.k8s_credentials: str = data.get("k8s-credentials") + self.model_configs: Dict[str, Any] = data.get("model-configs", {}) + + +class VcaDataMissingError(Exception): + """Data missing exception.""" + + +class VcaRequires(Object): + """Requires part of the vca relation. + + Attributes: + endpoint_name: Endpoint name of the charm for the vca relation. + data: Vca data from the relation. + """ + + def __init__(self, charm: CharmBase, endpoint_name: str = "vca") -> None: + super().__init__(charm, endpoint_name) + self._charm = charm + self.endpoint_name = endpoint_name + self.framework.observe(charm.on[endpoint_name].relation_changed, self._on_relation_changed) + + @property + def data(self) -> Optional[VcaData]: + """Vca data from the relation.""" + relation: Relation = self.model.get_relation(self.endpoint_name) + if not relation or relation.app not in relation.data: + logger.debug("no application data in the event") + return + + relation_data: Dict = dict(relation.data[relation.app]) + relation_data["model-configs"] = json.loads(relation_data.get("model-configs", "{}")) + try: + self._validate_relation_data(relation_data) + return VcaData(relation_data) + except VcaDataMissingError as e: + logger.warning(e) + + def _on_relation_changed(self, event: RelationChangedEvent) -> None: + if event.app not in event.relation.data: + logger.debug("no application data in the event") + return + + relation_data = event.relation.data[event.app] + try: + self._validate_relation_data(relation_data) + self._charm.on.vca_data_changed.emit() + except VcaDataMissingError as e: + logger.warning(e) + + def _validate_relation_data(self, relation_data: Dict[str, str]) -> None: + if not all(required_key in relation_data for required_key in RELATION_MANDATORY_KEYS): + raise VcaDataMissingError("vca data not ready yet") + + clouds = ("lxd-cloud", "k8s-cloud") + if not any(cloud in relation_data for cloud in clouds): + raise VcaDataMissingError("no clouds defined yet") + + +class VcaProvides(Object): + """Provides part of the vca relation. + + Attributes: + endpoint_name: Endpoint name of the charm for the vca relation. + """ + + def __init__(self, charm: CharmBase, endpoint_name: str = "vca") -> None: + super().__init__(charm, endpoint_name) + self.endpoint_name = endpoint_name + + def update_vca_data(self, vca_data: VcaData) -> None: + """Update vca data in relation. + + Args: + vca_data: VcaData object. + """ + relation: Relation + for relation in self.model.relations[self.endpoint_name]: + if not relation or self.model.app not in relation.data: + logger.debug("relation app data not ready yet") + for key, value in vca_data.data.items(): + if key == "model-configs": + value = json.dumps(value) + relation.data[self.model.app][key] = value diff --git a/installers/charm/vca-integrator-operator/metadata.yaml b/installers/charm/vca-integrator-operator/metadata.yaml new file mode 100644 index 00000000..bcc4375e --- /dev/null +++ b/installers/charm/vca-integrator-operator/metadata.yaml @@ -0,0 +1,30 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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. +####################################################################################### + +name: osm-vca-integrator +display-name: OSM VCA Integrator +summary: Deploy VCA integrator Operator Charm +description: | + This Operator deploys the vca-integrator charm that + facilitates the integration between OSM charms and + the VCA (Juju controller). +maintainers: + - David Garcia + +provides: + vca: + interface: osm-vca diff --git a/installers/charm/vca-integrator-operator/pyproject.toml b/installers/charm/vca-integrator-operator/pyproject.toml new file mode 100644 index 00000000..2e1a6dd2 --- /dev/null +++ b/installers/charm/vca-integrator-operator/pyproject.toml @@ -0,0 +1,56 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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. +####################################################################################### + +# 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 = 99 +target-version = ["py38"] + +[tool.isort] +profile = "black" + +# Linting tools configuration +[tool.flake8] +max-line-length = 99 +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", "E402", "E501", "D107"] +# D100, D101, D102, D103: Ignore missing docstrings in tests +per-file-ignores = ["tests/*:D100,D101,D102,D103,D104"] +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" + +[tool.bandit] +tests = ["B201", "B301"] diff --git a/installers/charm/vca-integrator-operator/requirements-dev.txt b/installers/charm/vca-integrator-operator/requirements-dev.txt new file mode 100644 index 00000000..65d82b91 --- /dev/null +++ b/installers/charm/vca-integrator-operator/requirements-dev.txt @@ -0,0 +1,16 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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. +####################################################################################### \ No newline at end of file diff --git a/installers/charm/vca-integrator-operator/requirements.txt b/installers/charm/vca-integrator-operator/requirements.txt new file mode 100644 index 00000000..66e845a3 --- /dev/null +++ b/installers/charm/vca-integrator-operator/requirements.txt @@ -0,0 +1,19 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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. +####################################################################################### +ops >= 1.2.0 +juju +pyyaml \ No newline at end of file diff --git a/installers/charm/vca-integrator-operator/src/charm.py b/installers/charm/vca-integrator-operator/src/charm.py new file mode 100755 index 00000000..34cb4f93 --- /dev/null +++ b/installers/charm/vca-integrator-operator/src/charm.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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. +####################################################################################### + +"""VcaIntegrator K8s charm module.""" + +import asyncio +import base64 +import logging +import os +from pathlib import Path +from typing import Dict, Set + +import yaml +from charms.osm_vca_integrator.v0.vca import VcaData, VcaProvides +from juju.controller import Controller +from ops.charm import CharmBase +from ops.main import main +from ops.model import ActiveStatus, BlockedStatus, StatusBase + +logger = logging.getLogger(__name__) + +GO_COOKIES = "/root/.go-cookies" +JUJU_DATA = os.environ["JUJU_DATA"] = "/root/.local/share/juju" +JUJU_CONFIGS = { + "public-key": "ssh/juju_id_rsa.pub", + "controllers": "controllers.yaml", + "accounts": "accounts.yaml", +} + + +class CharmError(Exception): + """Charm Error Exception.""" + + def __init__(self, message: str, status_class: StatusBase = BlockedStatus) -> None: + self.message = message + self.status_class = status_class + self.status = status_class(message) + + +class VcaIntegratorCharm(CharmBase): + """VcaIntegrator K8s Charm operator.""" + + def __init__(self, *args): + super().__init__(*args) + self.vca_provider = VcaProvides(self) + # Observe charm events + event_observe_mapping = { + self.on.config_changed: self._on_config_changed, + self.on.vca_relation_joined: self._on_config_changed, + } + for event, observer in event_observe_mapping.items(): + self.framework.observe(event, observer) + + # --------------------------------------------------------------------------- + # Properties + # --------------------------------------------------------------------------- + + @property + def clouds_set(self) -> Set: + """Clouds set in the configuration.""" + clouds_set = set() + for cloud_config in ["k8s-cloud", "lxd-cloud"]: + if cloud_name := self.config.get(cloud_config): + clouds_set.add(cloud_name.split(":")[0]) + return clouds_set + + @property + def vca_data(self) -> VcaData: + """Get VCA data.""" + return VcaData(self._get_vca_data()) + + # --------------------------------------------------------------------------- + # Handlers for Charm Events + # --------------------------------------------------------------------------- + + def _on_config_changed(self, _) -> None: + """Handler for the config-changed event.""" + # Validate charm configuration + try: + self._validate_config() + self._write_controller_config_files() + self._check_controller() + self.vca_provider.update_vca_data(self.vca_data) + self.unit.status = ActiveStatus() + except CharmError as e: + self.unit.status = e.status + + # --------------------------------------------------------------------------- + # Validation and configuration + # --------------------------------------------------------------------------- + + def _validate_config(self) -> None: + """Validate charm configuration. + + Raises: + Exception: if charm configuration is invalid. + """ + # Check mandatory fields + for mandatory_field in [ + "controllers", + "accounts", + "public-key", + ]: + if not self.config.get(mandatory_field): + raise CharmError(f'missing config: "{mandatory_field}"') + # Check if any clouds are set + if not self.clouds_set: + raise CharmError("no clouds set") + + if self.config.get("model-configs"): + try: + yaml.safe_load(self.config["model-configs"]) + except Exception: + raise CharmError("invalid yaml format for model-configs") + + def _write_controller_config_files(self) -> None: + Path(f"{JUJU_DATA}/ssh").mkdir(parents=True, exist_ok=True) + go_cookies = Path(GO_COOKIES) + if not go_cookies.is_file(): + go_cookies.write_text(data="[]") + for config, path in JUJU_CONFIGS.items(): + Path(f"{JUJU_DATA}/{path}").expanduser().write_text(self.config[config]) + + def _check_controller(self): + loop = asyncio.get_event_loop() + # Check controller connectivity + loop.run_until_complete(self._check_controller_connectivity()) + # Check clouds exist in controller + loop.run_until_complete(self._check_clouds_in_controller()) + + async def _check_controller_connectivity(self): + controller = Controller() + await controller.connect() + await controller.disconnect() + + async def _check_clouds_in_controller(self): + controller = Controller() + await controller.connect() + try: + controller_clouds = await controller.clouds() + for cloud in self.clouds_set: + if f"cloud-{cloud}" not in controller_clouds.clouds: + raise CharmError(f"Cloud {cloud} does not exist in the controller") + finally: + await controller.disconnect() + + def _get_vca_data(self) -> Dict[str, str]: + loop = asyncio.get_event_loop() + data_from_config = self._get_vca_data_from_config() + coro_data_from_controller = loop.run_until_complete(self._get_vca_data_from_controller()) + vca_data = {**data_from_config, **coro_data_from_controller} + logger.debug(f"vca data={vca_data}") + return vca_data + + def _get_vca_data_from_config(self) -> Dict[str, str]: + data = {"public-key": self.config["public-key"]} + if self.config.get("lxd-cloud"): + lxd_cloud_parts = self.config["lxd-cloud"].split(":") + data.update( + { + "lxd-cloud": lxd_cloud_parts[0], + "lxd-credentials": lxd_cloud_parts[1] + if len(lxd_cloud_parts) > 1 + else lxd_cloud_parts[0], + } + ) + if self.config.get("k8s-cloud"): + k8s_cloud_parts = self.config["k8s-cloud"].split(":") + data.update( + { + "k8s-cloud": k8s_cloud_parts[0], + "k8s-credentials": k8s_cloud_parts[1] + if len(k8s_cloud_parts) > 1 + else k8s_cloud_parts[0], + } + ) + if self.config.get("model-configs"): + data["model-configs"] = yaml.safe_load(self.config["model-configs"]) + + return data + + async def _get_vca_data_from_controller(self) -> Dict[str, str]: + controller = Controller() + await controller.connect() + try: + connection = controller._connector._connection + return { + "endpoints": ",".join(await controller.api_endpoints), + "user": connection.username, + "secret": connection.password, + "cacert": base64.b64encode(connection.cacert.encode("utf-8")).decode("utf-8"), + } + finally: + await controller.disconnect() + + +if __name__ == "__main__": # pragma: no cover + main(VcaIntegratorCharm) diff --git a/installers/charm/vca-integrator-operator/tests/integration/test_charm.py b/installers/charm/vca-integrator-operator/tests/integration/test_charm.py new file mode 100644 index 00000000..ebd43f56 --- /dev/null +++ b/installers/charm/vca-integrator-operator/tests/integration/test_charm.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +import logging +from pathlib import Path + +import pytest +import yaml +from pytest_operator.plugin import OpsTest + +logger = logging.getLogger(__name__) + +METADATA = yaml.safe_load(Path("./metadata.yaml").read_text()) + + +@pytest.mark.abort_on_fail +async def test_build_and_deploy(ops_test: OpsTest): + """Build the charm osm-vca-integrator-k8s and deploy it together with related charms. + + Assert on the unit status before any relations/configurations take place. + """ + await ops_test.model.set_config({"update-status-hook-interval": "10s"}) + + charm = await ops_test.build_charm(".") + await ops_test.model.deploy(charm, application_name="osm-vca-integrator-k8s") + await ops_test.model.wait_for_idle( + apps=["osm-vca-integrator-k8s"], status="blocked", timeout=1000 + ) + assert ( + ops_test.model.applications["osm-vca-integrator-k8s"].units[0].workload_status == "blocked" + ) + + logger.debug("Setting update-status-hook-interval to 60m") + await ops_test.model.set_config({"update-status-hook-interval": "60m"}) diff --git a/installers/charm/vca-integrator-operator/tests/unit/test_charm.py b/installers/charm/vca-integrator-operator/tests/unit/test_charm.py new file mode 100644 index 00000000..5018675d --- /dev/null +++ b/installers/charm/vca-integrator-operator/tests/unit/test_charm.py @@ -0,0 +1,34 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +import pytest +from ops.testing import Harness +from pytest_mock import MockerFixture + +from charm import VcaIntegratorCharm + + +@pytest.fixture +def harness(): + osm_vca_integrator_harness = Harness(VcaIntegratorCharm) + osm_vca_integrator_harness.begin() + yield osm_vca_integrator_harness + osm_vca_integrator_harness.cleanup() + + +def test_on_config_changed(mocker: MockerFixture, harness: Harness): + pass diff --git a/installers/charm/vca-integrator-operator/tox.ini b/installers/charm/vca-integrator-operator/tox.ini new file mode 100644 index 00000000..1893353c --- /dev/null +++ b/installers/charm/vca-integrator-operator/tox.ini @@ -0,0 +1,106 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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. +####################################################################################### + +[tox] +skipsdist=True +skip_missing_interpreters = True +envlist = lint, unit + +[vars] +src_path = {toxinidir}/src/ +tst_path = {toxinidir}/tests/ +lib_path = {toxinidir}/lib/charms/osm_vca_integrator +all_path = {[vars]src_path} {[vars]tst_path} {[vars]lib_path} + +[testenv] +setenv = + PYTHONPATH = {toxinidir}:{toxinidir}/lib:{[vars]src_path} + PYTHONBREAKPOINT=ipdb.set_trace + PY_COLORS=1 +passenv = + PYTHONPATH + CHARM_BUILD_DIR + MODEL_SETTINGS + +[testenv:fmt] +description = Apply coding style standards to code +deps = + black + isort +commands = + isort {[vars]all_path} + black {[vars]all_path} + +[testenv:lint] +description = Check code against coding style standards +deps = + black + flake8 + flake8-docstrings + flake8-copyright + flake8-builtins + pylint + pyproject-flake8 + pep8-naming + isort + codespell + yamllint + -r{toxinidir}/requirements.txt +commands = + codespell {[vars]lib_path} + codespell {toxinidir}/. --skip {toxinidir}/.git --skip {toxinidir}/.tox \ + --skip {toxinidir}/build --skip {toxinidir}/lib --skip {toxinidir}/venv \ + --skip {toxinidir}/.mypy_cache --skip {toxinidir}/icon.svg + pylint -E {[vars]src_path} + # pflake8 wrapper supports config from pyproject.toml + pflake8 {[vars]all_path} + isort --check-only --diff {[vars]all_path} + black --check --diff {[vars]all_path} + +[testenv:unit] +description = Run unit tests +deps = + pytest + pytest-mock + coverage[toml] + -r{toxinidir}/requirements.txt +commands = + coverage run --source={[vars]src_path},{[vars]lib_path} \ + -m pytest --ignore={[vars]tst_path}integration -v --tb native -s {posargs} + coverage report + coverage xml + +[testenv:security] +description = Run security tests +deps = + bandit + safety +commands = + bandit -r {[vars]src_path} + bandit -r {[vars]lib_path} + - safety check + +[testenv:integration] +description = Run integration tests +deps = + pytest + juju + pytest-operator + -r{toxinidir}/requirements.txt + -r{toxinidir}/requirements-dev.txt +commands = + pytest -v --tb native --ignore={[vars]tst_path}unit --log-cli-level=INFO -s {posargs}