Feature 11071: Modular OSM installation. Remove charms, juju and lxd 69/15169/8
authorgarciadeblas <gerardo.garciadeblas@telefonica.com>
Thu, 27 Mar 2025 11:09:00 +0000 (12:09 +0100)
committergarciadeblas <gerardo.garciadeblas@telefonica.com>
Tue, 17 Jun 2025 10:54:51 +0000 (12:54 +0200)
Change-Id: Icb380fa26a7e50dc59cd6d06b6099bfe16dcda08
Signed-off-by: garciadeblas <gerardo.garciadeblas@telefonica.com>
351 files changed:
devops-stages/stage-test.sh
installers/charm/README.md [deleted file]
installers/charm/bundles/.gitignore [deleted file]
installers/charm/bundles/osm-ha/README.md [deleted file]
installers/charm/bundles/osm-ha/bundle.yaml [deleted file]
installers/charm/bundles/osm-ha/charmcraft.yaml [deleted file]
installers/charm/bundles/osm/CODE_OF_CONDUCT.md [deleted file]
installers/charm/bundles/osm/CONTRIBUTING.md [deleted file]
installers/charm/bundles/osm/README.md [deleted file]
installers/charm/bundles/osm/bundle.yaml [deleted file]
installers/charm/bundles/osm/charmcraft.yaml [deleted file]
installers/charm/generate_bundle.py [deleted file]
installers/charm/grafana/.gitignore [deleted file]
installers/charm/grafana/.jujuignore [deleted file]
installers/charm/grafana/.yamllint.yaml [deleted file]
installers/charm/grafana/README.md [deleted file]
installers/charm/grafana/charmcraft.yaml [deleted file]
installers/charm/grafana/config.yaml [deleted file]
installers/charm/grafana/icon.svg [deleted file]
installers/charm/grafana/metadata.yaml [deleted file]
installers/charm/grafana/requirements-test.txt [deleted file]
installers/charm/grafana/requirements.txt [deleted file]
installers/charm/grafana/src/charm.py [deleted file]
installers/charm/grafana/src/pod_spec.py [deleted file]
installers/charm/grafana/templates/default_dashboards.yaml [deleted file]
installers/charm/grafana/templates/default_datasources.yaml [deleted file]
installers/charm/grafana/templates/kafka_exporter_dashboard.json [deleted file]
installers/charm/grafana/templates/mongodb_exporter_dashboard.json [deleted file]
installers/charm/grafana/templates/mysql_exporter_dashboard.json [deleted file]
installers/charm/grafana/templates/nodes_exporter_dashboard.json [deleted file]
installers/charm/grafana/templates/summary_dashboard.json [deleted file]
installers/charm/grafana/tests/__init__.py [deleted file]
installers/charm/grafana/tests/test_charm.py [deleted file]
installers/charm/grafana/tests/test_pod_spec.py [deleted file]
installers/charm/grafana/tox.ini [deleted file]
installers/charm/juju-simplestreams-operator/.gitignore [deleted file]
installers/charm/juju-simplestreams-operator/.jujuignore [deleted file]
installers/charm/juju-simplestreams-operator/CONTRIBUTING.md [deleted file]
installers/charm/juju-simplestreams-operator/LICENSE [deleted file]
installers/charm/juju-simplestreams-operator/README.md [deleted file]
installers/charm/juju-simplestreams-operator/actions.yaml [deleted file]
installers/charm/juju-simplestreams-operator/charmcraft.yaml [deleted file]
installers/charm/juju-simplestreams-operator/config.yaml [deleted file]
installers/charm/juju-simplestreams-operator/files/juju-metadata [deleted file]
installers/charm/juju-simplestreams-operator/files/nginx.conf [deleted file]
installers/charm/juju-simplestreams-operator/lib/charms/nginx_ingress_integrator/v0/ingress.py [deleted file]
installers/charm/juju-simplestreams-operator/lib/charms/observability_libs/v1/kubernetes_service_patch.py [deleted file]
installers/charm/juju-simplestreams-operator/lib/charms/osm_libs/v0/utils.py [deleted file]
installers/charm/juju-simplestreams-operator/metadata.yaml [deleted file]
installers/charm/juju-simplestreams-operator/pyproject.toml [deleted file]
installers/charm/juju-simplestreams-operator/requirements.txt [deleted file]
installers/charm/juju-simplestreams-operator/src/charm.py [deleted file]
installers/charm/juju-simplestreams-operator/tests/unit/test_charm.py [deleted file]
installers/charm/juju-simplestreams-operator/tox.ini [deleted file]
installers/charm/kafka-exporter/.gitignore [deleted file]
installers/charm/kafka-exporter/.jujuignore [deleted file]
installers/charm/kafka-exporter/.yamllint.yaml [deleted file]
installers/charm/kafka-exporter/README.md [deleted file]
installers/charm/kafka-exporter/charmcraft.yaml [deleted file]
installers/charm/kafka-exporter/config.yaml [deleted file]
installers/charm/kafka-exporter/lib/charms/kafka_k8s/v0/kafka.py [deleted file]
installers/charm/kafka-exporter/metadata.yaml [deleted file]
installers/charm/kafka-exporter/requirements-test.txt [deleted file]
installers/charm/kafka-exporter/requirements.txt [deleted file]
installers/charm/kafka-exporter/src/charm.py [deleted file]
installers/charm/kafka-exporter/src/pod_spec.py [deleted file]
installers/charm/kafka-exporter/templates/kafka_exporter_dashboard.json [deleted file]
installers/charm/kafka-exporter/tests/__init__.py [deleted file]
installers/charm/kafka-exporter/tests/test_charm.py [deleted file]
installers/charm/kafka-exporter/tests/test_pod_spec.py [deleted file]
installers/charm/kafka-exporter/tox.ini [deleted file]
installers/charm/local_osm_bundle.yaml [deleted file]
installers/charm/local_osm_bundle_proxy.yaml [deleted file]
installers/charm/local_osm_ha_bundle.yaml [deleted file]
installers/charm/mariadb-k8s/.gitignore [deleted file]
installers/charm/mariadb-k8s/.yamllint.yaml [deleted file]
installers/charm/mariadb-k8s/README.md [deleted file]
installers/charm/mariadb-k8s/actions.yaml [deleted file]
installers/charm/mariadb-k8s/actions/backup [deleted file]
installers/charm/mariadb-k8s/actions/remove-backup [deleted file]
installers/charm/mariadb-k8s/actions/restore [deleted file]
installers/charm/mariadb-k8s/charmcraft.yaml [deleted file]
installers/charm/mariadb-k8s/config.yaml [deleted file]
installers/charm/mariadb-k8s/icon.svg [deleted file]
installers/charm/mariadb-k8s/layer.yaml [deleted file]
installers/charm/mariadb-k8s/metadata.yaml [deleted file]
installers/charm/mariadb-k8s/reactive/osm_mariadb.py [deleted file]
installers/charm/mariadb-k8s/reactive/spec_template.yaml [deleted file]
installers/charm/mariadb-k8s/reactive/spec_template_ha.yaml [deleted file]
installers/charm/mariadb-k8s/test-requirements.txt [deleted file]
installers/charm/mariadb-k8s/tests/basic_deployment.py [deleted file]
installers/charm/mariadb-k8s/tests/bundles/mariadb-ha.yaml [deleted file]
installers/charm/mariadb-k8s/tests/bundles/mariadb.yaml [deleted file]
installers/charm/mariadb-k8s/tests/tests.yaml [deleted file]
installers/charm/mariadb-k8s/tox.ini [deleted file]
installers/charm/mongodb-exporter/.gitignore [deleted file]
installers/charm/mongodb-exporter/.jujuignore [deleted file]
installers/charm/mongodb-exporter/.yamllint.yaml [deleted file]
installers/charm/mongodb-exporter/README.md [deleted file]
installers/charm/mongodb-exporter/charmcraft.yaml [deleted file]
installers/charm/mongodb-exporter/config.yaml [deleted file]
installers/charm/mongodb-exporter/metadata.yaml [deleted file]
installers/charm/mongodb-exporter/requirements-test.txt [deleted file]
installers/charm/mongodb-exporter/requirements.txt [deleted file]
installers/charm/mongodb-exporter/src/charm.py [deleted file]
installers/charm/mongodb-exporter/src/pod_spec.py [deleted file]
installers/charm/mongodb-exporter/templates/mongodb_exporter_dashboard.json [deleted file]
installers/charm/mongodb-exporter/tests/__init__.py [deleted file]
installers/charm/mongodb-exporter/tests/test_charm.py [deleted file]
installers/charm/mongodb-exporter/tests/test_pod_spec.py [deleted file]
installers/charm/mongodb-exporter/tox.ini [deleted file]
installers/charm/mysqld-exporter/.gitignore [deleted file]
installers/charm/mysqld-exporter/.jujuignore [deleted file]
installers/charm/mysqld-exporter/.yamllint.yaml [deleted file]
installers/charm/mysqld-exporter/README.md [deleted file]
installers/charm/mysqld-exporter/charmcraft.yaml [deleted file]
installers/charm/mysqld-exporter/config.yaml [deleted file]
installers/charm/mysqld-exporter/metadata.yaml [deleted file]
installers/charm/mysqld-exporter/requirements-test.txt [deleted file]
installers/charm/mysqld-exporter/requirements.txt [deleted file]
installers/charm/mysqld-exporter/src/charm.py [deleted file]
installers/charm/mysqld-exporter/src/pod_spec.py [deleted file]
installers/charm/mysqld-exporter/templates/mysql_exporter_dashboard.json [deleted file]
installers/charm/mysqld-exporter/tests/__init__.py [deleted file]
installers/charm/mysqld-exporter/tests/test_charm.py [deleted file]
installers/charm/mysqld-exporter/tests/test_pod_spec.py [deleted file]
installers/charm/mysqld-exporter/tox.ini [deleted file]
installers/charm/osm-keystone/.gitignore [deleted file]
installers/charm/osm-keystone/.jujuignore [deleted file]
installers/charm/osm-keystone/CONTRIBUTING.md [deleted file]
installers/charm/osm-keystone/LICENSE [deleted file]
installers/charm/osm-keystone/README.md [deleted file]
installers/charm/osm-keystone/actions.yaml [deleted file]
installers/charm/osm-keystone/charmcraft.yaml [deleted file]
installers/charm/osm-keystone/config.yaml [deleted file]
installers/charm/osm-keystone/lib/charms/observability_libs/v0/kubernetes_service_patch.py [deleted file]
installers/charm/osm-keystone/metadata.yaml [deleted file]
installers/charm/osm-keystone/pyproject.toml [deleted file]
installers/charm/osm-keystone/requirements.txt [deleted file]
installers/charm/osm-keystone/src/charm.py [deleted file]
installers/charm/osm-keystone/src/cluster.py [deleted file]
installers/charm/osm-keystone/src/config.py [deleted file]
installers/charm/osm-keystone/src/interfaces.py [deleted file]
installers/charm/osm-keystone/tests/integration/test_charm.py [deleted file]
installers/charm/osm-keystone/tests/unit/test_charm.py [deleted file]
installers/charm/osm-keystone/tox.ini [deleted file]
installers/charm/osm-lcm/.gitignore [deleted file]
installers/charm/osm-lcm/.jujuignore [deleted file]
installers/charm/osm-lcm/CONTRIBUTING.md [deleted file]
installers/charm/osm-lcm/LICENSE [deleted file]
installers/charm/osm-lcm/README.md [deleted file]
installers/charm/osm-lcm/actions.yaml [deleted file]
installers/charm/osm-lcm/charmcraft.yaml [deleted file]
installers/charm/osm-lcm/config.yaml [deleted file]
installers/charm/osm-lcm/files/vscode-workspace.json [deleted file]
installers/charm/osm-lcm/lib/charms/data_platform_libs/v0/data_interfaces.py [deleted file]
installers/charm/osm-lcm/lib/charms/kafka_k8s/v0/kafka.py [deleted file]
installers/charm/osm-lcm/lib/charms/osm_libs/v0/utils.py [deleted file]
installers/charm/osm-lcm/lib/charms/osm_ro/v0/ro.py [deleted file]
installers/charm/osm-lcm/lib/charms/osm_vca_integrator/v0/vca.py [deleted file]
installers/charm/osm-lcm/metadata.yaml [deleted file]
installers/charm/osm-lcm/pyproject.toml [deleted file]
installers/charm/osm-lcm/requirements.txt [deleted file]
installers/charm/osm-lcm/src/charm.py [deleted file]
installers/charm/osm-lcm/src/legacy_interfaces.py [deleted file]
installers/charm/osm-lcm/tests/integration/test_charm.py [deleted file]
installers/charm/osm-lcm/tests/unit/test_charm.py [deleted file]
installers/charm/osm-lcm/tox.ini [deleted file]
installers/charm/osm-mon/.gitignore [deleted file]
installers/charm/osm-mon/.jujuignore [deleted file]
installers/charm/osm-mon/CONTRIBUTING.md [deleted file]
installers/charm/osm-mon/LICENSE [deleted file]
installers/charm/osm-mon/README.md [deleted file]
installers/charm/osm-mon/actions.yaml [deleted file]
installers/charm/osm-mon/charmcraft.yaml [deleted file]
installers/charm/osm-mon/config.yaml [deleted file]
installers/charm/osm-mon/files/vscode-workspace.json [deleted file]
installers/charm/osm-mon/lib/charms/data_platform_libs/v0/data_interfaces.py [deleted file]
installers/charm/osm-mon/lib/charms/kafka_k8s/v0/kafka.py [deleted file]
installers/charm/osm-mon/lib/charms/observability_libs/v1/kubernetes_service_patch.py [deleted file]
installers/charm/osm-mon/lib/charms/osm_libs/v0/utils.py [deleted file]
installers/charm/osm-mon/lib/charms/osm_vca_integrator/v0/vca.py [deleted file]
installers/charm/osm-mon/metadata.yaml [deleted file]
installers/charm/osm-mon/pyproject.toml [deleted file]
installers/charm/osm-mon/requirements.txt [deleted file]
installers/charm/osm-mon/src/charm.py [deleted file]
installers/charm/osm-mon/src/legacy_interfaces.py [deleted file]
installers/charm/osm-mon/tests/integration/test_charm.py [deleted file]
installers/charm/osm-mon/tests/unit/test_charm.py [deleted file]
installers/charm/osm-mon/tox.ini [deleted file]
installers/charm/osm-nbi/.gitignore [deleted file]
installers/charm/osm-nbi/.jujuignore [deleted file]
installers/charm/osm-nbi/CONTRIBUTING.md [deleted file]
installers/charm/osm-nbi/LICENSE [deleted file]
installers/charm/osm-nbi/README.md [deleted file]
installers/charm/osm-nbi/actions.yaml [deleted file]
installers/charm/osm-nbi/charmcraft.yaml [deleted file]
installers/charm/osm-nbi/config.yaml [deleted file]
installers/charm/osm-nbi/files/vscode-workspace.json [deleted file]
installers/charm/osm-nbi/lib/charms/data_platform_libs/v0/data_interfaces.py [deleted file]
installers/charm/osm-nbi/lib/charms/kafka_k8s/v0/kafka.py [deleted file]
installers/charm/osm-nbi/lib/charms/nginx_ingress_integrator/v0/ingress.py [deleted file]
installers/charm/osm-nbi/lib/charms/observability_libs/v1/kubernetes_service_patch.py [deleted file]
installers/charm/osm-nbi/lib/charms/osm_libs/v0/utils.py [deleted file]
installers/charm/osm-nbi/lib/charms/osm_nbi/v0/nbi.py [deleted file]
installers/charm/osm-nbi/metadata.yaml [deleted file]
installers/charm/osm-nbi/pyproject.toml [deleted file]
installers/charm/osm-nbi/requirements.txt [deleted file]
installers/charm/osm-nbi/src/charm.py [deleted file]
installers/charm/osm-nbi/src/legacy_interfaces.py [deleted file]
installers/charm/osm-nbi/tests/integration/test_charm.py [deleted file]
installers/charm/osm-nbi/tests/unit/test_charm.py [deleted file]
installers/charm/osm-nbi/tox.ini [deleted file]
installers/charm/osm-ng-ui/.gitignore [deleted file]
installers/charm/osm-ng-ui/.jujuignore [deleted file]
installers/charm/osm-ng-ui/CONTRIBUTING.md [deleted file]
installers/charm/osm-ng-ui/LICENSE [deleted file]
installers/charm/osm-ng-ui/README.md [deleted file]
installers/charm/osm-ng-ui/actions.yaml [deleted file]
installers/charm/osm-ng-ui/charmcraft.yaml [deleted file]
installers/charm/osm-ng-ui/config.yaml [deleted file]
installers/charm/osm-ng-ui/lib/charms/nginx_ingress_integrator/v0/ingress.py [deleted file]
installers/charm/osm-ng-ui/lib/charms/observability_libs/v1/kubernetes_service_patch.py [deleted file]
installers/charm/osm-ng-ui/lib/charms/osm_libs/v0/utils.py [deleted file]
installers/charm/osm-ng-ui/lib/charms/osm_nbi/v0/nbi.py [deleted file]
installers/charm/osm-ng-ui/metadata.yaml [deleted file]
installers/charm/osm-ng-ui/pyproject.toml [deleted file]
installers/charm/osm-ng-ui/requirements.txt [deleted file]
installers/charm/osm-ng-ui/src/charm.py [deleted file]
installers/charm/osm-ng-ui/tests/integration/test_charm.py [deleted file]
installers/charm/osm-ng-ui/tests/unit/test_charm.py [deleted file]
installers/charm/osm-ng-ui/tox.ini [deleted file]
installers/charm/osm-pol/.gitignore [deleted file]
installers/charm/osm-pol/.jujuignore [deleted file]
installers/charm/osm-pol/CONTRIBUTING.md [deleted file]
installers/charm/osm-pol/LICENSE [deleted file]
installers/charm/osm-pol/README.md [deleted file]
installers/charm/osm-pol/actions.yaml [deleted file]
installers/charm/osm-pol/charmcraft.yaml [deleted file]
installers/charm/osm-pol/config.yaml [deleted file]
installers/charm/osm-pol/files/vscode-workspace.json [deleted file]
installers/charm/osm-pol/lib/charms/data_platform_libs/v0/data_interfaces.py [deleted file]
installers/charm/osm-pol/lib/charms/kafka_k8s/v0/kafka.py [deleted file]
installers/charm/osm-pol/lib/charms/osm_libs/v0/utils.py [deleted file]
installers/charm/osm-pol/metadata.yaml [deleted file]
installers/charm/osm-pol/pyproject.toml [deleted file]
installers/charm/osm-pol/requirements.txt [deleted file]
installers/charm/osm-pol/src/charm.py [deleted file]
installers/charm/osm-pol/src/legacy_interfaces.py [deleted file]
installers/charm/osm-pol/tests/integration/test_charm.py [deleted file]
installers/charm/osm-pol/tests/unit/test_charm.py [deleted file]
installers/charm/osm-pol/tox.ini [deleted file]
installers/charm/osm-ro/.gitignore [deleted file]
installers/charm/osm-ro/.jujuignore [deleted file]
installers/charm/osm-ro/CONTRIBUTING.md [deleted file]
installers/charm/osm-ro/LICENSE [deleted file]
installers/charm/osm-ro/README.md [deleted file]
installers/charm/osm-ro/actions.yaml [deleted file]
installers/charm/osm-ro/charmcraft.yaml [deleted file]
installers/charm/osm-ro/config.yaml [deleted file]
installers/charm/osm-ro/files/vscode-workspace.json [deleted file]
installers/charm/osm-ro/lib/charms/data_platform_libs/v0/data_interfaces.py [deleted file]
installers/charm/osm-ro/lib/charms/kafka_k8s/v0/kafka.py [deleted file]
installers/charm/osm-ro/lib/charms/observability_libs/v1/kubernetes_service_patch.py [deleted file]
installers/charm/osm-ro/lib/charms/osm_libs/v0/utils.py [deleted file]
installers/charm/osm-ro/lib/charms/osm_ro/v0/ro.py [deleted file]
installers/charm/osm-ro/metadata.yaml [deleted file]
installers/charm/osm-ro/pyproject.toml [deleted file]
installers/charm/osm-ro/requirements.txt [deleted file]
installers/charm/osm-ro/src/charm.py [deleted file]
installers/charm/osm-ro/src/legacy_interfaces.py [deleted file]
installers/charm/osm-ro/tests/integration/test_charm.py [deleted file]
installers/charm/osm-ro/tests/unit/test_charm.py [deleted file]
installers/charm/osm-ro/tox.ini [deleted file]
installers/charm/osm-update-db-operator/.gitignore [deleted file]
installers/charm/osm-update-db-operator/.jujuignore [deleted file]
installers/charm/osm-update-db-operator/CONTRIBUTING.md [deleted file]
installers/charm/osm-update-db-operator/LICENSE [deleted file]
installers/charm/osm-update-db-operator/README.md [deleted file]
installers/charm/osm-update-db-operator/actions.yaml [deleted file]
installers/charm/osm-update-db-operator/charmcraft.yaml [deleted file]
installers/charm/osm-update-db-operator/config.yaml [deleted file]
installers/charm/osm-update-db-operator/metadata.yaml [deleted file]
installers/charm/osm-update-db-operator/pyproject.toml [deleted file]
installers/charm/osm-update-db-operator/requirements.txt [deleted file]
installers/charm/osm-update-db-operator/src/charm.py [deleted file]
installers/charm/osm-update-db-operator/src/db_upgrade.py [deleted file]
installers/charm/osm-update-db-operator/tests/integration/test_charm.py [deleted file]
installers/charm/osm-update-db-operator/tests/unit/test_charm.py [deleted file]
installers/charm/osm-update-db-operator/tests/unit/test_db_upgrade.py [deleted file]
installers/charm/osm-update-db-operator/tox.ini [deleted file]
installers/charm/prometheus/.gitignore [deleted file]
installers/charm/prometheus/.jujuignore [deleted file]
installers/charm/prometheus/.yamllint.yaml [deleted file]
installers/charm/prometheus/README.md [deleted file]
installers/charm/prometheus/actions.yaml [deleted file]
installers/charm/prometheus/charmcraft.yaml [deleted file]
installers/charm/prometheus/config.yaml [deleted file]
installers/charm/prometheus/icon.svg [deleted file]
installers/charm/prometheus/metadata.yaml [deleted file]
installers/charm/prometheus/requirements-test.txt [deleted file]
installers/charm/prometheus/requirements.txt [deleted file]
installers/charm/prometheus/src/charm.py [deleted file]
installers/charm/prometheus/src/pod_spec.py [deleted file]
installers/charm/prometheus/tests/__init__.py [deleted file]
installers/charm/prometheus/tests/test_charm.py [deleted file]
installers/charm/prometheus/tests/test_pod_spec.py [deleted file]
installers/charm/prometheus/tox.ini [deleted file]
installers/charm/vca-integrator-operator/.gitignore [deleted file]
installers/charm/vca-integrator-operator/.jujuignore [deleted file]
installers/charm/vca-integrator-operator/CONTRIBUTING.md [deleted file]
installers/charm/vca-integrator-operator/LICENSE [deleted file]
installers/charm/vca-integrator-operator/README.md [deleted file]
installers/charm/vca-integrator-operator/actions.yaml [deleted file]
installers/charm/vca-integrator-operator/charmcraft.yaml [deleted file]
installers/charm/vca-integrator-operator/config.yaml [deleted file]
installers/charm/vca-integrator-operator/lib/charms/osm_vca_integrator/v0/vca.py [deleted file]
installers/charm/vca-integrator-operator/metadata.yaml [deleted file]
installers/charm/vca-integrator-operator/pyproject.toml [deleted file]
installers/charm/vca-integrator-operator/requirements-dev.txt [deleted file]
installers/charm/vca-integrator-operator/requirements.txt [deleted file]
installers/charm/vca-integrator-operator/src/charm.py [deleted file]
installers/charm/vca-integrator-operator/tests/integration/test_charm.py [deleted file]
installers/charm/vca-integrator-operator/tests/unit/test_charm.py [deleted file]
installers/charm/vca-integrator-operator/tox.ini [deleted file]
installers/charm/zookeeper-k8s/.gitignore [deleted file]
installers/charm/zookeeper-k8s/.yamllint.yaml [deleted file]
installers/charm/zookeeper-k8s/README.md [deleted file]
installers/charm/zookeeper-k8s/config.yaml [deleted file]
installers/charm/zookeeper-k8s/icon.svg [deleted file]
installers/charm/zookeeper-k8s/layer.yaml [deleted file]
installers/charm/zookeeper-k8s/metadata.yaml [deleted file]
installers/charm/zookeeper-k8s/reactive/spec_template.yaml [deleted file]
installers/charm/zookeeper-k8s/reactive/zookeeper.py [deleted file]
installers/charm/zookeeper-k8s/test-requirements.txt [deleted file]
installers/charm/zookeeper-k8s/tests/basic_deployment.py [deleted file]
installers/charm/zookeeper-k8s/tests/bundles/zookeeper-ha.yaml [deleted file]
installers/charm/zookeeper-k8s/tests/bundles/zookeeper.yaml [deleted file]
installers/charm/zookeeper-k8s/tests/tests.yaml [deleted file]
installers/charm/zookeeper-k8s/tox.ini [deleted file]
installers/charmed_install.sh [deleted file]
installers/charmed_uninstall.sh [deleted file]
installers/full_install_osm.sh
installers/install_juju.sh [deleted file]
installers/install_lxd.sh [deleted file]
installers/install_osm.sh
installers/uninstall_osm.sh
installers/update-juju-lxc-images [deleted file]
tools/debug/charmed/README.md [deleted file]
tools/debug/charmed/generate_ssh_config.sh [deleted file]
tools/promote-charms-and-snaps.sh [deleted file]

index 8edc11c..c48f693 100755 (executable)
 
 set -eu
 
-CURRENT_DIR=`pwd`
-
-# Execute tests for charms
-CHARM_PATH="./installers/charm"
-NEW_CHARMS_NAMES="osm-keystone osm-lcm osm-mon osm-nbi osm-ng-ui osm-pol osm-ro vca-integrator-operator"
-OLD_CHARMS_NAMES="prometheus grafana"
-LOCAL_GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
-GERRIT_BRANCH=${GERRIT_BRANCH:-${LOCAL_GIT_BRANCH}}
-for charm in $NEW_CHARMS_NAMES; do
-    if [ $(git diff --name-only "origin/${GERRIT_BRANCH}" -- "installers/charm/${charm}" | wc -l) -ne 0 ]; then
-        echo "Running tox for ${charm}"
-        cd "${CHARM_PATH}/${charm}"
-        TOX_PARALLEL_NO_SPINNER=1 tox -e lint,unit --parallel=auto
-        cd "${CURRENT_DIR}"
-    fi
-done
-for charm in $OLD_CHARMS_NAMES; do
-    if [ $(git diff --name-only "origin/${GERRIT_BRANCH}" -- "installers/charm/${charm}" | wc -l) -ne 0 ]; then
-        echo "Running tox for ${charm}"
-        cd "${CHARM_PATH}/${charm}"
-        TOX_PARALLEL_NO_SPINNER=1 tox --parallel=auto
-        cd "${CURRENT_DIR}"
-    fi
-done
-
 # Download helm chart dependencies
 helm dependency update installers/helm/osm
 
diff --git a/installers/charm/README.md b/installers/charm/README.md
deleted file mode 100644 (file)
index a8467b0..0000000
+++ /dev/null
@@ -1,170 +0,0 @@
-<!--
- Copyright 2020 Canonical Ltd.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
-     http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-# OSM Charms and interfaces
-
-**Description**: This document describes the high-level view of the OSM Charms and interfaces. An important note is that these charms Kubernetes Charms, so they must be deployed on top of a Kubernetes Cloud using Juju.
-
-## Folder tree
-
-In the current directory, there is one folder "interfaces" that has all the interfaces of the OSM components, which are basically two: osm-nbi, and osm-ro.
-
-Additionally, we can see six folders that contain each OSM core components: lcm-k8s, mon-k8s, nbi-k8s, pol-k8s, ro-k8s, and ui-k8s.
-
-Then, we can see a folder "bundle" which has the templates for the OSM bundles in single instance and HA.
-
-The "layers" folder include one common layer for all the osm charms (osm-common)
-
-```txt
-
-├── bundles
-│   ├── osm
-│   └── osm-ha
-├── interfaces
-│   ├── osm-nbi
-│   └── osm-ro
-├── layers
-│   └── osm-common
-├── lcm-k8s
-├── mon-k8s
-├── nbi-k8s
-├── pol-k8s
-├── ro-k8s
-├── ui-k8s
-└── ng-ui --> new operator framework
-
-```
-
-## Charms
-
-All the charms have a very similar structure. This subsection explains the purpose of each file inside the charms, as well as basic steps to get started.
-
-The folder structure for each charm looks like this:
-
-```txt
-<charm>-k8s/
-├── config.yaml
-├── icon.svg
-├── layer.yaml
-├── metadata.yaml
-├── reactive
-│   ├── <charm>.py
-│   └── spec_template.yaml
-├── README.md
-├── .gitignore
-├── .yamllint.yaml
-└── tox.ini
-```
-
-Purpose of each file:
-
-- **config.yaml**: YAML file that include all the configurable options for the charm.
-- **icon.svg**: SVG icon. This is the icon that will appear in the Charm Store.
-- **layer.yaml**: YAML file with the layers that the charm needs. All the OSM Charms need at least the following layers: caas-base, status, leadership, and osm-common. If charms provide or require interfaces, which all of them do, those interfaces should be specified in this file too.
-- **metadata.yaml**: YAML file that describe the top level information of the charm: name, description, series, interfaces that provide/require, needed storage, and deployment type.
-- **reactive/\<charm>.py**: Python file that implements the actual logic to the charm.
-- **reactive/spec_template.yaml**: Pod spec template to be used by the pods.
-- **README.md**: This file describes how to build the charm, how to prepare the environment to test it with Microk8s.
-- **.gitignore**: Typical Git Ignore file, to avoid pushing unwanted files to upstream.
-- **.yamllint.yaml**: YAML file to specify the files to exclude from the yamllint test that tox.ini does.
-- **tox.ini**: Includes basic functions to build the charms, and check the linting.
-
-## Interfaces
-
-Each interface needs at least three files:
-
-- **interface.yaml:** Metadata of the interface: name, maintainer, and summary.
-- **provides.py:** Code for the charm that provides the interface.
-- **requires.py:** Code for the charm that requires the interface.
-
-Additionally, there are also files for copyright and a README that explains how to use the interface.
-
-# Steps for testing
-
-## Dependencies
-
-```bash
-sudo apt install tox -y
-```
-
-## Check the syntax of the charms
-
-```bash
-./lint.sh
-```
-
-## Build all the charms
-
-```bash
-./build.sh
-```
-
-## Generate bundle
-
-```bash
-# Generate bundle from built charms
-python3 generate_bundle.py --local --destination osm.yaml
-# Help
-python3 generate_bundle.py --help
-```
-
-## Install VCA
-
-```bash
-sudo snap install juju --classic
-juju bootstrap localhost osm-lxd
-```
-
-## Generate overlay
-
-> NOTE: This will be removed once the installer is merged.
-
-```bash
-sudo snap install osmclient
-sudo snap alias osmclient.osm osm
-sudo snap connect osmclient:juju-client-observe
-sudo snap connect osmclient:ssh-public-keys
-sudo snap connect osmclient:network-control
-osmclient.overlay  # Execute the commands printed by this command to enable native charms
-```
-
-## Bootstrap Juju controller in Microk8s
-
-```bash
-sudo snap install microk8s --classic
-sudo usermod -a -G microk8s ubuntu
-sudo chown -f -R ubuntu ~/.kube
-newgrp microk8s
-microk8s.status --wait-ready
-microk8s.enable storage dns  # (metallb) is optional
-juju bootstrap microk8s osm-k8s
-```
-
-## Deploy OSM with charms
-
-```bash
-juju add-model osm
-juju deploy ./osm.yaml --overlay vca-overlay.yaml
-```
-
-## Wait until Charms are deployed
-
-```bash
-watch -c juju status --color  # Wait until every application is in active state
-export OSM_HOSTNAME=<ip-nbi>
-osm ns-list
-# ...
-```
diff --git a/installers/charm/bundles/.gitignore b/installers/charm/bundles/.gitignore
deleted file mode 100644 (file)
index 00b9f63..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-# Copyright 2022 ETSI
-#
-# 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.
-
-*.zip
-*build/
\ No newline at end of file
diff --git a/installers/charm/bundles/osm-ha/README.md b/installers/charm/bundles/osm-ha/README.md
deleted file mode 100644 (file)
index 05dab9d..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-<!--
- Copyright 2020 Canonical Ltd.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
-     http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-# Installation
-
-Go to the [OSM User Guide](https://osm.etsi.org/docs/user-guide/03-installing-osm.html#charmed-installation) for the highly available production-grade deployment.
-For a more minimal cluster suitable for testing, deploy the [single instance OSM bundle](https://jaas.ai/osm/bundle).
-
-# Bundle Components
-
-- [grafana](https://jaas.ai/u/charmed-osm/grafana/0): A CAAS charm to deploy grafana for metrics visualization
-- [kafka k8s](https://jaas.ai/u/charmed-osm/kafka-k8s): A CAAS charm to deploy Kafka used as a messaging bus between OSM components
-- [lcm](https://jaas.ai/u/charmed-osm/lcm/0): A CAAS charm to deploy OSM's Lifecycle Management (LCM) component responsible for network services orchestration.
-- [mariadb k8s](https://jaas.ai/u/charmed-osm/mariadb-k8s): A Juju charm deploying and managing database server (MariaDB) on Kubernetes
-- [mon](https://jaas.ai/u/charmed-osm/mon/0): A CAAS charm to deploy OSM's Monitoring Interface (MON) responsible for metrics collection
-- [mongodb k8s](https://jaas.ai/u/charmed-osm/mongodb-k8s): A CAAS charm to deploy MongoDB responsible for structuring the data
-- [nbi](https://jaas.ai/u/charmed-osm/nbi/5): A juju charm to deploy OSM's Northbound Interface (NBI) on Kubernetes.
-- [pol](https://jaas.ai/u/charmed-osm/pol/0): A CAAS charm to deploy OSM's Policy Module (POL) responsible for configuring alarms and actions
-- [prometheus](https://jaas.ai/u/charmed-osm/prometheus): A CAAS charm to deploy Prometheus.
-- [ro](https://jaas.ai/u/charmed-osm/ro/0): A CAAS charm to deploy OSM's Resource Orchestrator (RO) responsible for the life cycle management of VIM resources.
-- [ng-ui](https://jaas.ai/u/charmed-osm/ng-ui): A CAAS charm to deploy OSM's User Interface (UI)
-- [zookeeper k8s](https://jaas.ai/u/charmed-osm/zookeeper-k8s): A CAAS charm to deploy zookeeper for distributed synchronization
-
-# Troubleshooting
-
-If you have any trouble with the installation, please contact us, we will be glad to answer your questions.
-
-You can directly contact the team:
-
-- Guillermo Calvino ([guillermo.calvino@canonical.com](guillermo.calvino@canonical.com))
-- Gulsum Atici ([gulsum.atici@canonical.com](gulsum.atici@canonical.com))
-- Mark Beierl ([mark.beierl@canonical.com](mark.beierl@canonical.com))
-- Patricia Reinoso ([patricia.reinoso@canonical.com](patricia.reinoso@canonical.com))
-- Wajeeha Hamid ([wajeeha.hamid@canonical.com](wajeeha.hamid@canonical.com))
diff --git a/installers/charm/bundles/osm-ha/bundle.yaml b/installers/charm/bundles/osm-ha/bundle.yaml
deleted file mode 100644 (file)
index a4e84d3..0000000
+++ /dev/null
@@ -1,196 +0,0 @@
-# Copyright 2020 Canonical Ltd.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-#     Unless required by applicable law or agreed to in writing, software
-#     distributed under the License is distributed on an "AS IS" BASIS,
-#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#     See the License for the specific language governing permissions and
-#     limitations under the License.
-name: osm-ha
-bundle: kubernetes
-docs: https://discourse.charmhub.io/t/osm-docs-index/8806
-description: |
-  **A high-available Charmed OSM cluster**
-
-  Charmed OSM is an OSM distribution, developed and maintained by Canonical, which uses
-  Juju charms to simplify its deployments and operations. This bundle distribution enables
-  TSPs to easily deploy pure upstream OSM in highly available, production-grade, and
-  scalable clusters.
-
-  - Industry‐aligned and fully compliant with upstream
-  - Predictable release cadence and upgrade path
-  - Simplified deployments and operations
-  - Stable and secure
-  - Highly Available and resilient against failures
-  - Supported with Ubuntu Advantage
-  - Availability of managed services
-applications:
-  zookeeper:
-    charm: zookeeper-k8s
-    channel: latest/stable
-    scale: 3
-    storage:
-      data: 100M
-  kafka:
-    charm: kafka-k8s
-    channel: latest/stable
-    scale: 3
-    trust: true
-    storage:
-      data: 100M
-  mariadb:
-    charm: charmed-osm-mariadb-k8s
-    scale: 3
-    series: kubernetes
-    storage:
-      database: 300M
-    options:
-      password: manopw
-      root_password: osm4u
-      user: mano
-      ha-mode: true
-  mongodb:
-    charm: mongodb-k8s
-    channel: 5/edge
-    scale: 3
-    series: kubernetes
-    storage:
-      mongodb: 50M
-  nbi:
-    charm: osm-nbi
-    channel: latest/beta
-    trust: true
-    scale: 3
-    options:
-      database-commonkey: osm
-      log-level: DEBUG
-    resources:
-      nbi-image: opensourcemano/nbi:testing-daily
-  ro:
-    charm: osm-ro
-    channel: latest/beta
-    trust: true
-    scale: 3
-    options:
-      log-level: DEBUG
-    resources:
-      ro-image: opensourcemano/ro:testing-daily
-  ng-ui:
-    charm: osm-ng-ui
-    channel: latest/beta
-    trust: true
-    scale: 3
-    resources:
-      ng-ui-image: opensourcemano/ng-ui:testing-daily
-  lcm:
-    charm: osm-lcm
-    channel: latest/beta
-    scale: 3
-    options:
-      database-commonkey: osm
-      log-level: DEBUG
-    resources:
-      lcm-image: opensourcemano/lcm:testing-daily
-  mon:
-    charm: osm-mon
-    channel: latest/beta
-    trust: true
-    scale: 1
-    options:
-      database-commonkey: osm
-      log-level: DEBUG
-      keystone-enabled: true
-    resources:
-      mon-image: opensourcemano/mon:testing-daily
-  pol:
-    charm: osm-pol
-    channel: latest/beta
-    scale: 3
-    options:
-      log-level: DEBUG
-    resources:
-      pol-image: opensourcemano/pol:testing-daily
-  vca:
-    charm: osm-vca-integrator
-    channel: latest/beta
-    scale: 1
-  ingress:
-    charm: nginx-ingress-integrator
-    channel: latest/stable
-    scale: 3
-  prometheus:
-    charm: osm-prometheus
-    channel: latest/stable
-    scale: 1
-    series: kubernetes
-    storage:
-      data: 50M
-    options:
-      default-target: "mon:8000"
-  grafana:
-    charm: osm-grafana
-    channel: latest/stable
-    scale: 3
-    series: kubernetes
-  keystone:
-    charm: osm-keystone
-    channel: latest/beta
-    scale: 1
-    resources:
-      keystone-image: opensourcemano/keystone:testing-daily
-relations:
-  - - grafana:prometheus
-    - prometheus:prometheus
-  - - kafka:zookeeper
-    - zookeeper:zookeeper
-  - - keystone:db
-    - mariadb:mysql
-  - - lcm:kafka
-    - kafka:kafka
-  - - lcm:mongodb
-    - mongodb:database
-  - - lcm:vca
-    - vca:vca
-  - - ro:ro
-    - lcm:ro
-  - - ro:kafka
-    - kafka:kafka
-  - - ro:mongodb
-    - mongodb:database
-  - - pol:kafka
-    - kafka:kafka
-  - - pol:mongodb
-    - mongodb:database
-  - - mon:mongodb
-    - mongodb:database
-  - - mon:kafka
-    - kafka:kafka
-  - - mon:vca
-    - vca:vca
-  - - nbi:mongodb
-    - mongodb:database
-  - - nbi:kafka
-    - kafka:kafka
-  - - nbi:ingress
-    - ingress:ingress
-  - - nbi:prometheus
-    - prometheus:prometheus
-  - - nbi:keystone
-    - keystone:keystone
-  - - mon:prometheus
-    - prometheus:prometheus
-  - - ng-ui:nbi
-    - nbi:nbi
-  - - ng-ui:ingress
-    - ingress:ingress
-  - - mon:keystone
-    - keystone:keystone
-  - - mariadb:mysql
-    - pol:mysql
-  - - grafana:db
-    - mariadb:mysql
diff --git a/installers/charm/bundles/osm-ha/charmcraft.yaml b/installers/charm/bundles/osm-ha/charmcraft.yaml
deleted file mode 100644 (file)
index 111b05c..0000000
+++ /dev/null
@@ -1,14 +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.
-type: bundle
diff --git a/installers/charm/bundles/osm/CODE_OF_CONDUCT.md b/installers/charm/bundles/osm/CODE_OF_CONDUCT.md
deleted file mode 100644 (file)
index 121dcc8..0000000
+++ /dev/null
@@ -1,96 +0,0 @@
-<!-- Copyright 2022 ETSI
-
-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.
-
-For those usages not covered by the Apache License, Version 2.0 please
-contact: legal@canonical.com
-
-To get in touch with the maintainers, please contact:
-osm-charmers@lists.launchpad.net -->
-# Contributor Covenant Code of Conduct
-
-## Our Pledge
-
-In the interest of fostering an open and welcoming environment, we as
-contributors and maintainers pledge to making participation in our project and
-our community a harassment-free experience for everyone, regardless of age, body
-size, disability, ethnicity, sex characteristics, gender identity and expression,
-level of experience, education, socio-economic status, nationality, personal
-appearance, race, religion, or sexual identity and orientation.
-
-## Our Standards
-
-Examples of behavior that contributes to creating a positive environment
-include:
-
-* Using welcoming and inclusive language
-* Being respectful of differing viewpoints and experiences
-* Gracefully accepting constructive criticism
-* Focusing on what is best for the community
-* Showing empathy towards other community members
-
-Examples of unacceptable behavior by participants include:
-
-* The use of sexualized language or imagery and unwelcome sexual attention or
- advances
-* Trolling, insulting/derogatory comments, and personal or political attacks
-* Public or private harassment
-* Publishing others' private information, such as a physical or electronic
- address, without explicit permission
-* Other conduct which could reasonably be considered inappropriate in a
- professional setting
-
-## Our Responsibilities
-
-Project maintainers are responsible for clarifying the standards of acceptable
-behavior and are expected to take appropriate and fair corrective action in
-response to any instances of unacceptable behavior.
-
-Project maintainers have the right and responsibility to remove, edit, or
-reject comments, commits, code, wiki edits, issues, and other contributions
-that are not aligned to this Code of Conduct, or to ban temporarily or
-permanently any contributor for other behaviors that they deem inappropriate,
-threatening, offensive, or harmful.
-
-## Scope
-
-This Code of Conduct applies both within project spaces and in public spaces
-when an individual is representing the project or its community. Examples of
-representing a project or community include using an official project e-mail
-address, posting via an official social media account, or acting as an appointed
-representative at an online or offline event. Representation of a project may be
-further defined and clarified by project maintainers.
-
-## Enforcement
-
-Instances of abusive, harassing, or otherwise unacceptable behavior may be
-reported by contacting the project team in the 
-[OSM public mattermost channel](https://chat.charmhub.io/charmhub/channels/charmed-osm). 
-All complaints will be reviewed and investigated and will result in a response that
-is deemed necessary and appropriate to the circumstances. The project team is
-obligated to maintain confidentiality with regard to the reporter of an incident.
-Further details of specific enforcement policies may be posted separately.
-
-Project maintainers who do not follow or enforce the Code of Conduct in good
-faith may face temporary or permanent repercussions as determined by other
-members of the project's leadership.
-
-## Attribution
-
-This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
-available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
-
-[homepage]: https://www.contributor-covenant.org
-
-For answers to common questions about this code of conduct, see
-https://www.contributor-covenant.org/faq
\ No newline at end of file
diff --git a/installers/charm/bundles/osm/CONTRIBUTING.md b/installers/charm/bundles/osm/CONTRIBUTING.md
deleted file mode 100644 (file)
index 63c6178..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-<!-- Copyright 2022 ETSI
-
-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.
-
-For those usages not covered by the Apache License, Version 2.0 please
-contact: legal@canonical.com
-
-To get in touch with the maintainers, please contact:
-osm-charmers@lists.launchpad.net -->
-
-# Contributing
-
-## Overview
-
-This documents explains the processes and practices recommended for contributing enhancements to
-this bundle.
-
-- Generally, before developing enhancements to this charm, you should consider [opening an issue
-  ](https://osm.etsi.org/bugzilla/enter_bug.cgi?product=OSM) explaining your use case. (Component=devops, version=master)
-- If you would like to chat with us about your use-cases or proposed implementation, you can reach
-  us at [OSM Juju public channel](https://opensourcemano.slack.com/archives/C027KJGPECA).
-- 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 gerrit patch onto
-  the `master` branch.
-
-## Code Repository
-
-To clone the repository for this bundle:
-
-```shell
-git clone "https://osm.etsi.org/gerrit/osm/devops"
-```
-
-The bundle can be found in the following directory:
-
-```shell
-cd devops/installers/charm/bundles/osm
-```
diff --git a/installers/charm/bundles/osm/README.md b/installers/charm/bundles/osm/README.md
deleted file mode 100644 (file)
index c6fb07f..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-<!--
- Copyright 2020 Canonical Ltd.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
-     http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-# Installation
-
-Charmed OSM runs on the Ubuntu Long Term Support (LTS) release Bionic. Additionally, we recommend installing on a freshly installed virtual machine or bare metal with minimum requirements of:
-
-- **16 GB RAM**
-- **4 CPUs**
-- **50 GB** of free storage space
-
-The steps needed for the bundle installation are as follows:
-
-- Installing MicroK8s and Juju
-- Setting up the MicroK8s and LXD
-- Bootstrapping the Juju controller
-- Deploying the charmed OSM bundle
-- Installing OSM client
-- Integration of charmed OSM with MicroStack VIM
-
-Follow the installation steps [here](https://juju.is/tutorials/charmed-osm-get-started#1-introduction)
-
-# Bundle Components
-
-- [grafana](https://jaas.ai/u/charmed-osm/grafana/0): A CAAS charm to deploy grafana for metrics visualization
-- [kafka k8s](https://jaas.ai/u/charmed-osm/kafka-k8s): A CAAS charm to deploy Kafka used as a messaging bus between OSM components
-- [lcm](https://jaas.ai/u/charmed-osm/lcm/0): A CAAS charm to deploy OSM's Lifecycle Management (LCM) component responsible for network services orchestration.
-- [mariadb k8s](https://jaas.ai/u/charmed-osm/mariadb-k8s): A Juju charm deploying and managing database server (MariaDB) on Kubernetes
-- [mon](https://jaas.ai/u/charmed-osm/mon/0): A CAAS charm to deploy OSM's Monitoring Interface (MON) responsible for metrics collection
-- [mongodb k8s](https://jaas.ai/u/charmed-osm/mongodb-k8s): A CAAS charm to deploy MongoDB responsible for structuring the data
-- [nbi](https://jaas.ai/u/charmed-osm/nbi/5): A juju charm to deploy OSM's Northbound Interface (NBI) on Kubernetes.
-- [pol](https://jaas.ai/u/charmed-osm/pol/0): A CAAS charm to deploy OSM's Policy Module (POL) responsible for configuring alarms and actions
-- [prometheus](https://jaas.ai/u/charmed-osm/prometheus): A CAAS charm to deploy Prometheus.
-- [ro](https://jaas.ai/u/charmed-osm/ro/0): A CAAS charm to deploy OSM's Resource Orchestrator (RO) responsible for the life cycle management of VIM resources.
-- [ng-ui](https://jaas.ai/u/charmed-osm/ng-ui): A CAAS charm to deploy OSM's User Interface (UI)
-- [zookeeper k8s](https://jaas.ai/u/charmed-osm/zookeeper-k8s): A CAAS charm to deploy zookeeper for distributed synchronization
-
-# Troubleshooting
-
-If you have any trouble with the installation, please contact us, we will be glad to answer your questions.
-
-You can directly contact the team:
-
-- Guillermo Calvino ([guillermo.calvino@canonical.com](guillermo.calvino@canonical.com))
-- Gulsum Atici ([gulsum.atici@canonical.com](gulsum.atici@canonical.com))
-- Mark Beierl ([mark.beierl@canonical.com](mark.beierl@canonical.com))
-- Patricia Reinoso ([patricia.reinoso@canonical.com](patricia.reinoso@canonical.com))
-- Wajeeha Hamid ([wajeeha.hamid@canonical.com](wajeeha.hamid@canonical.com))
diff --git a/installers/charm/bundles/osm/bundle.yaml b/installers/charm/bundles/osm/bundle.yaml
deleted file mode 100644 (file)
index b2db446..0000000
+++ /dev/null
@@ -1,195 +0,0 @@
-# Copyright 2020 Canonical Ltd.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-#     Unless required by applicable law or agreed to in writing, software
-#     distributed under the License is distributed on an "AS IS" BASIS,
-#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#     See the License for the specific language governing permissions and
-#     limitations under the License.
-name: osm
-bundle: kubernetes
-docs: https://discourse.charmhub.io/t/osm-docs-index/8806
-issues: https://osm.etsi.org/bugzilla/
-description: |
-  **Single instance Charmed OSM**
-
-  Charmed OSM is an OSM distribution, developed and maintained by Canonical, which uses
-  Juju charms to simplify its deployments and operations. This bundle distribution refers
-  to the development stack for OSM and allows you to deploy a single instance OSM bundle
-  that is fast, reliable, and a complete solution with MicroStack and MicroK8s.
-
-  - Industry-aligned and fully compliant with upstream
-  - Predictable release cadence and upgrade path
-  - Simplified deployments and operations
-  - Stable and secure
-  - Supported with Ubuntu Advantage
-  - Availability of managed services
-applications:
-  zookeeper:
-    charm: zookeeper-k8s
-    channel: latest/stable
-    scale: 1
-    storage:
-      data: 100M
-  kafka:
-    charm: kafka-k8s
-    channel: latest/stable
-    scale: 1
-    trust: true
-    storage:
-      data: 100M
-  mariadb:
-    charm: charmed-osm-mariadb-k8s
-    scale: 1
-    series: kubernetes
-    storage:
-      database: 50M
-    options:
-      password: manopw
-      root_password: osm4u
-      user: mano
-  mongodb:
-    charm: mongodb-k8s
-    channel: 5/edge
-    scale: 1
-    series: kubernetes
-    storage:
-      mongodb: 50M
-  nbi:
-    charm: osm-nbi
-    channel: latest/beta
-    trust: true
-    scale: 1
-    options:
-      database-commonkey: osm
-      log-level: DEBUG
-    resources:
-      nbi-image: opensourcemano/nbi:testing-daily
-  ro:
-    charm: osm-ro
-    channel: latest/beta
-    trust: true
-    scale: 1
-    options:
-      log-level: DEBUG
-    resources:
-      ro-image: opensourcemano/ro:testing-daily
-  ng-ui:
-    charm: osm-ng-ui
-    channel: latest/beta
-    trust: true
-    scale: 1
-    resources:
-      ng-ui-image: opensourcemano/ng-ui:testing-daily
-  lcm:
-    charm: osm-lcm
-    channel: latest/beta
-    scale: 1
-    options:
-      database-commonkey: osm
-      log-level: DEBUG
-    resources:
-      lcm-image: opensourcemano/lcm:testing-daily
-  mon:
-    charm: osm-mon
-    channel: latest/beta
-    trust: true
-    scale: 1
-    options:
-      database-commonkey: osm
-      log-level: DEBUG
-      keystone-enabled: true
-    resources:
-      mon-image: opensourcemano/mon:testing-daily
-  pol:
-    charm: osm-pol
-    channel: latest/beta
-    scale: 1
-    options:
-      log-level: DEBUG
-    resources:
-      pol-image: opensourcemano/pol:testing-daily
-  vca:
-    charm: osm-vca-integrator
-    channel: latest/beta
-    scale: 1
-  ingress:
-    charm: nginx-ingress-integrator
-    channel: latest/stable
-    scale: 1
-  prometheus:
-    charm: osm-prometheus
-    channel: latest/stable
-    scale: 1
-    series: kubernetes
-    storage:
-      data: 50M
-    options:
-      default-target: "mon:8000"
-  grafana:
-    charm: osm-grafana
-    channel: latest/stable
-    scale: 1
-    series: kubernetes
-  keystone:
-    charm: osm-keystone
-    channel: latest/beta
-    scale: 1
-    resources:
-      keystone-image: opensourcemano/keystone:testing-daily
-relations:
-  - - grafana:prometheus
-    - prometheus:prometheus
-  - - kafka:zookeeper
-    - zookeeper:zookeeper
-  - - keystone:db
-    - mariadb:mysql
-  - - lcm:kafka
-    - kafka:kafka
-  - - lcm:mongodb
-    - mongodb:database
-  - - lcm:vca
-    - vca:vca
-  - - ro:ro
-    - lcm:ro
-  - - ro:kafka
-    - kafka:kafka
-  - - ro:mongodb
-    - mongodb:database
-  - - pol:kafka
-    - kafka:kafka
-  - - pol:mongodb
-    - mongodb:database
-  - - mon:mongodb
-    - mongodb:database
-  - - mon:kafka
-    - kafka:kafka
-  - - mon:vca
-    - vca:vca
-  - - nbi:mongodb
-    - mongodb:database
-  - - nbi:kafka
-    - kafka:kafka
-  - - nbi:ingress
-    - ingress:ingress
-  - - nbi:prometheus
-    - prometheus:prometheus
-  - - nbi:keystone
-    - keystone:keystone
-  - - mon:prometheus
-    - prometheus:prometheus
-  - - ng-ui:nbi
-    - nbi:nbi
-  - - ng-ui:ingress
-    - ingress:ingress
-  - - mon:keystone
-    - keystone:keystone
-  - - mariadb:mysql
-    - pol:mysql
-  - - grafana:db
-    - mariadb:mysql
diff --git a/installers/charm/bundles/osm/charmcraft.yaml b/installers/charm/bundles/osm/charmcraft.yaml
deleted file mode 100644 (file)
index 111b05c..0000000
+++ /dev/null
@@ -1,14 +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.
-type: bundle
diff --git a/installers/charm/generate_bundle.py b/installers/charm/generate_bundle.py
deleted file mode 100644 (file)
index a82e016..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-# Copyright 2020 Canonical Ltd.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-#     Unless required by applicable law or agreed to in writing, software
-#     distributed under the License is distributed on an "AS IS" BASIS,
-#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#     See the License for the specific language governing permissions and
-#     limitations under the License.
-import json
-import argparse
-
-CHANNEL_LIST = [
-    "stable",
-    "candidate",
-    "edge",
-]
-BUNDLE_PREFIX = "cs:~charmed-osm"
-DEFAULT_BUNDLE = "bundles/osm/bundle.yaml"
-HA_BUNDLE = "bundles/osm-ha/bundle.yaml"
-
-parser = argparse.ArgumentParser(description="Process some arguments.")
-
-parser.add_argument("--channel", help="Channel from the Charm Store")
-parser.add_argument("--destination", help="Destination for the generated bundle")
-parser.add_argument("--ha", help="Select HA bundle", action="store_true")
-parser.add_argument("--local", help="Path to the bundle directory", action="store_true")
-parser.add_argument("--store", help="Path to the bundle directory", action="store_true")
-
-args = parser.parse_args()
-print(args)
-if not args.local and not args.store:
-    raise Exception("--local or --store must be specified")
-if args.local and args.store:
-    raise Exception("Both --local and --store cannot be specified. Please choose one.")
-if not args.destination:
-    raise Exception("--destination must be specified")
-if args.channel and not args.channel in CHANNEL_LIST:
-    raise Exception(
-        "Channel {} does not exist. Please choose one of these: {}".format(
-            args.channel, CHANNEL_LIST
-        )
-    )
-channel = args.channel if args.channel else "stable"
-path = HA_BUNDLE if args.ha else DEFAULT_BUNDLE
-destination = args.destination
-prefix = "." if args.local else BUNDLE_PREFIX
-suffix = "/build" if args.local else ""
-
-data = {
-    "channel": channel,
-    "prefix": prefix,
-    "suffix": suffix,
-}
-
-with open(path) as template:
-    bundle_template = template.read()
-    template.close()
-with open("{}".format(destination), "w") as text_file:
-    text_file.write(bundle_template % data)
-    text_file.close()
diff --git a/installers/charm/grafana/.gitignore b/installers/charm/grafana/.gitignore
deleted file mode 100644 (file)
index 2885df2..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-venv
-.vscode
-build
-*.charm
-.coverage
-coverage.xml
-.stestr
-cover
-release
\ No newline at end of file
diff --git a/installers/charm/grafana/.jujuignore b/installers/charm/grafana/.jujuignore
deleted file mode 100644 (file)
index 3ae3e7d..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-venv
-.vscode
-build
-*.charm
-.coverage
-coverage.xml
-.gitignore
-.stestr
-cover
-release
-tests/
-requirements*
-tox.ini
diff --git a/installers/charm/grafana/.yamllint.yaml b/installers/charm/grafana/.yamllint.yaml
deleted file mode 100644 (file)
index 783a81d..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
----
-extends: default
-
-yaml-files:
-  - "*.yaml"
-  - "*.yml"
-  - ".yamllint"
-ignore: |
-  .tox
-  cover/
-  build/
-  venv
-  release/
-  templates/
diff --git a/installers/charm/grafana/README.md b/installers/charm/grafana/README.md
deleted file mode 100644 (file)
index 1cc1fb7..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<!-- 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.
-
-For those usages not covered by the Apache License, Version 2.0 please
-contact: legal@canonical.com
-
-To get in touch with the maintainers, please contact:
-osm-charmers@lists.launchpad.net -->
-
-# Grafana operator Charm for Kubernetes
-
-## Requirements
diff --git a/installers/charm/grafana/charmcraft.yaml b/installers/charm/grafana/charmcraft.yaml
deleted file mode 100644 (file)
index 0a285a9..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-type: charm
-bases:
-  - build-on:
-      - name: ubuntu
-        channel: "20.04"
-        architectures: ["amd64"]
-    run-on:
-      - name: ubuntu
-        channel: "20.04"
-        architectures:
-          - amd64
-          - aarch64
-          - arm64
-parts:
-  charm:
-    build-packages: [git]
diff --git a/installers/charm/grafana/config.yaml b/installers/charm/grafana/config.yaml
deleted file mode 100644 (file)
index 7f97f58..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-options:
-  max_file_size:
-    type: int
-    description: |
-      The maximum file size, in megabytes. If there is a reverse proxy in front
-      of Keystone, it may need to be configured to handle the requested size.
-      Note: if set to 0, there is no limit.
-    default: 0
-  ingress_class:
-    type: string
-    description: |
-      Ingress class name. This is useful for selecting the ingress to be used
-      in case there are multiple ingresses in the underlying k8s clusters.
-  ingress_whitelist_source_range:
-    type: string
-    description: |
-      A comma-separated list of CIDRs to store in the
-      ingress.kubernetes.io/whitelist-source-range annotation.
-
-      This can be used to lock down access to
-      Keystone based on source IP address.
-    default: ""
-  tls_secret_name:
-    type: string
-    description: TLS Secret name
-    default: ""
-  site_url:
-    type: string
-    description: Ingress URL
-    default: ""
-  cluster_issuer:
-    type: string
-    description: Name of the cluster issuer for TLS certificates
-    default: ""
-  osm_dashboards:
-    type: boolean
-    description: Enable OSM System monitoring dashboards
-    default: false
-  image_pull_policy:
-    type: string
-    description: |
-      ImagePullPolicy configuration for the pod.
-      Possible values: always, ifnotpresent, never
-    default: always
-  mysql_uri:
-    type: string
-    description: |
-      Mysql uri with the following format:
-        mysql://<user>:<pass>@<host>:<port>/<database>
-  admin_user:
-    type: string
-    description: Admin user
-    default: admin
-  log_level:
-    type: string
-    description: |
-      Logging level for Grafana. Options are “debug”, “info”,
-      “warn”, “error”, and “critical”.
-    default: info
-  port:
-    description: The port grafana-k8s will be listening on
-    type: int
-    default: 3000
-  security_context:
-    description: Enables the security context of the pods
-    type: boolean
-    default: false
diff --git a/installers/charm/grafana/icon.svg b/installers/charm/grafana/icon.svg
deleted file mode 100644 (file)
index 49c744d..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Generator: Adobe Illustrator 23.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
-<svg id="Layer_1" style="enable-background:new 0 0 85.12 92.46" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" height="250px" viewBox="0 0 85.12 92.46" width="250px" version="1.1" y="0px" x="0px" xmlns:xlink="http://www.w3.org/1999/xlink">
-<style type="text/css">
-       .st0{fill:url(#SVGID_1_);}
-</style>
-<linearGradient id="SVGID_1_" y2="28.783" gradientUnits="userSpaceOnUse" x2="42.562" y1="113.26" x1="42.562">
-       <stop stop-color="#FFF200" offset="0"/>
-       <stop stop-color="#F15A29" offset="1"/>
-</linearGradient>
-<path class="st0" d="m85.01 40.8c-0.14-1.55-0.41-3.35-0.93-5.32-0.51-1.97-1.28-4.13-2.39-6.37-1.12-2.24-2.57-4.57-4.47-6.82-0.74-0.88-1.54-1.76-2.42-2.6 1.3-5.17-1.59-9.65-1.59-9.65-4.98-0.31-8.14 1.54-9.31 2.39-0.2-0.08-0.39-0.17-0.59-0.25-0.85-0.34-1.72-0.66-2.61-0.95-0.89-0.28-1.81-0.54-2.74-0.76-0.94-0.22-1.89-0.4-2.86-0.55-0.17-0.03-0.34-0.05-0.51-0.07-2.18-6.95-8.41-9.85-8.41-9.85-6.95 4.41-8.27 10.57-8.27 10.57s-0.03 0.14-0.07 0.36c-0.38 0.11-0.77 0.22-1.15 0.34-0.53 0.16-1.06 0.36-1.59 0.55-0.53 0.21-1.06 0.41-1.58 0.64-1.05 0.45-2.09 0.96-3.1 1.53-0.99 0.55-1.95 1.16-2.9 1.82-0.14-0.06-0.24-0.11-0.24-0.11-9.62-3.68-18.17 0.75-18.17 0.75-0.78 10.24 3.84 16.68 4.76 17.86-0.23 0.63-0.44 1.27-0.64 1.92-0.71 2.32-1.24 4.7-1.57 7.16-0.05 0.35-0.09 0.71-0.13 1.07-8.9 4.38-11.53 13.38-11.53 13.38 7.42 8.53 16.07 9.06 16.07 9.06 0.01-0.01 0.02-0.01 0.02-0.02 1.1 1.96 2.37 3.83 3.8 5.57 0.6 0.73 1.23 1.43 1.88 2.11-2.71 7.74 0.38 14.18 0.38 14.18 8.26 0.31 13.69-3.61 14.83-4.52 0.82 0.28 1.66 0.53 2.5 0.74 2.54 0.65 5.14 1.04 7.74 1.15 0.65 0.03 1.3 0.04 1.95 0.04h0.31l0.21-0.01 0.41-0.01 0.4-0.02 0.01 0.01c3.89 5.55 10.74 6.34 10.74 6.34 4.87-5.13 5.15-10.22 5.15-11.33v-0.07-0.15s0 0 0 0c0-0.08-0.01-0.15-0.01-0.23 1.02-0.72 2-1.49 2.92-2.31 1.95-1.76 3.65-3.77 5.06-5.93 0.13-0.2 0.26-0.41 0.39-0.62 5.51 0.32 9.39-3.41 9.39-3.41-0.91-5.74-4.18-8.54-4.87-9.07 0 0-0.03-0.02-0.07-0.05s-0.06-0.05-0.06-0.05c-0.04-0.02-0.08-0.05-0.12-0.08 0.03-0.35 0.06-0.69 0.08-1.04 0.04-0.62 0.06-1.24 0.06-1.85v-0.46-0.23-0.12-0.16l-0.02-0.38-0.03-0.52c-0.01-0.18-0.02-0.34-0.04-0.5-0.01-0.16-0.03-0.32-0.05-0.48l-0.06-0.48-0.07-0.47c-0.09-0.63-0.21-1.26-0.36-1.88-0.58-2.47-1.54-4.82-2.82-6.93s-2.86-3.98-4.65-5.56-3.79-2.85-5.9-3.79c-2.1-0.95-4.31-1.55-6.51-1.83-1.1-0.14-2.2-0.2-3.28-0.19l-0.41 0.01h-0.1-0.14l-0.17 0.01-0.4 0.03c-0.15 0.01-0.31 0.02-0.45 0.04-0.56 0.05-1.11 0.13-1.66 0.23-2.18 0.41-4.24 1.2-6.06 2.28-1.82 1.09-3.39 2.45-4.68 3.98-1.28 1.54-2.28 3.24-2.96 5-0.69 1.76-1.07 3.58-1.18 5.35-0.03 0.44-0.04 0.88-0.03 1.32 0 0.11 0 0.22 0.01 0.33l0.01 0.35c0.02 0.21 0.03 0.42 0.05 0.63 0.09 0.9 0.25 1.75 0.49 2.58 0.48 1.66 1.25 3.15 2.2 4.43s2.08 2.33 3.28 3.15 2.49 1.41 3.76 1.79 2.54 0.54 3.74 0.53c0.15 0 0.3 0 0.44-0.01 0.08 0 0.16-0.01 0.24-0.01s0.16-0.01 0.24-0.01c0.13-0.01 0.25-0.03 0.38-0.04 0.03 0 0.07-0.01 0.11-0.01l0.12-0.02c0.08-0.01 0.15-0.02 0.23-0.03 0.16-0.02 0.29-0.05 0.43-0.08s0.28-0.05 0.42-0.09c0.27-0.06 0.54-0.14 0.8-0.22 0.52-0.17 1.01-0.38 1.46-0.61s0.87-0.5 1.26-0.77c0.11-0.08 0.22-0.16 0.33-0.25 0.42-0.33 0.48-0.94 0.15-1.35-0.29-0.36-0.79-0.45-1.19-0.23-0.1 0.05-0.2 0.11-0.3 0.16-0.35 0.17-0.71 0.32-1.09 0.45-0.39 0.12-0.79 0.22-1.2 0.29-0.21 0.03-0.42 0.06-0.63 0.08-0.11 0.01-0.21 0.02-0.32 0.02s-0.22 0.01-0.32 0.01-0.21 0-0.31-0.01c-0.13-0.01-0.26-0.01-0.39-0.02h-0.01-0.04l-0.09 0.02c-0.06-0.01-0.12-0.01-0.17-0.02-0.12-0.01-0.23-0.03-0.35-0.04-0.93-0.13-1.88-0.4-2.79-0.82-0.91-0.41-1.79-0.98-2.57-1.69-0.79-0.71-1.48-1.56-2.01-2.52-0.54-0.96-0.92-2.03-1.09-3.16-0.09-0.56-0.13-1.14-0.11-1.71 0.01-0.16 0.01-0.31 0.02-0.47v-0.03-0.06l0.01-0.12c0.01-0.08 0.01-0.15 0.02-0.23 0.03-0.31 0.08-0.62 0.13-0.92 0.43-2.45 1.65-4.83 3.55-6.65 0.47-0.45 0.98-0.87 1.53-1.25 0.55-0.37 1.12-0.7 1.73-0.98 0.6-0.28 1.23-0.5 1.88-0.68 0.65-0.17 1.31-0.29 1.98-0.35 0.34-0.03 0.67-0.04 1.01-0.04h0.23l0.27 0.01 0.17 0.01h0.03 0.07l0.27 0.02c0.73 0.06 1.46 0.16 2.17 0.32 1.43 0.32 2.83 0.85 4.13 1.57 2.6 1.44 4.81 3.69 6.17 6.4 0.69 1.35 1.16 2.81 1.4 4.31 0.06 0.38 0.1 0.76 0.13 1.14l0.02 0.29 0.01 0.29c0.01 0.1 0.01 0.19 0.01 0.29 0 0.09 0.01 0.2 0 0.27v0.25l-0.01 0.28c-0.01 0.19-0.02 0.49-0.03 0.67-0.03 0.42-0.07 0.83-0.12 1.24s-0.12 0.82-0.19 1.22c-0.08 0.4-0.17 0.81-0.27 1.21-0.2 0.8-0.46 1.59-0.76 2.36-0.61 1.54-1.42 3-2.4 4.36-1.96 2.7-4.64 4.9-7.69 6.29-1.52 0.69-3.13 1.19-4.78 1.47-0.82 0.14-1.66 0.22-2.5 0.25l-0.15 0.01h-0.13-0.27-0.41-0.21-0.01-0.08c-0.45-0.01-0.9-0.03-1.34-0.07-1.79-0.13-3.55-0.45-5.27-0.95-1.71-0.49-3.38-1.16-4.95-2-3.14-1.68-5.95-3.98-8.15-6.76-1.11-1.38-2.07-2.87-2.87-4.43s-1.42-3.2-1.89-4.88c-0.46-1.68-0.75-3.39-0.86-5.12l-0.02-0.32-0.01-0.08v-0.07-0.14l-0.01-0.28v-0.07-0.1-0.2l-0.01-0.4v-0.08-0.03-0.16c0-0.21 0.01-0.42 0.01-0.63 0.03-0.85 0.1-1.73 0.21-2.61s0.26-1.76 0.44-2.63 0.39-1.74 0.64-2.59c0.49-1.71 1.1-3.36 1.82-4.92 1.44-3.12 3.34-5.88 5.61-8.09 0.57-0.55 1.16-1.08 1.77-1.57s1.25-0.95 1.9-1.37c0.65-0.43 1.32-0.82 2.02-1.18 0.34-0.19 0.7-0.35 1.05-0.52 0.18-0.08 0.36-0.16 0.53-0.24 0.18-0.08 0.36-0.16 0.54-0.23 0.72-0.3 1.46-0.56 2.21-0.8 0.19-0.06 0.38-0.11 0.56-0.17 0.19-0.06 0.38-0.1 0.57-0.16 0.38-0.11 0.76-0.2 1.14-0.29 0.19-0.05 0.39-0.08 0.58-0.13 0.19-0.04 0.38-0.08 0.58-0.12 0.19-0.04 0.39-0.07 0.58-0.11l0.29-0.05 0.29-0.04c0.2-0.03 0.39-0.06 0.59-0.09 0.22-0.04 0.44-0.05 0.66-0.09 0.18-0.02 0.48-0.06 0.65-0.08 0.14-0.01 0.28-0.03 0.41-0.04l0.28-0.03 0.14-0.01 0.16-0.01c0.22-0.01 0.44-0.03 0.66-0.04l0.33-0.02h0.02 0.07l0.14-0.01c0.19-0.01 0.38-0.02 0.56-0.03 0.75-0.02 1.5-0.02 2.24 0 1.48 0.06 2.93 0.22 4.34 0.48 2.82 0.53 5.49 1.43 7.89 2.62 2.41 1.18 4.57 2.63 6.44 4.2 0.12 0.1 0.23 0.2 0.35 0.3 0.11 0.1 0.23 0.2 0.34 0.3 0.23 0.2 0.44 0.41 0.66 0.61s0.43 0.41 0.64 0.62c0.2 0.21 0.41 0.41 0.61 0.63 0.8 0.84 1.53 1.69 2.19 2.55 1.33 1.71 2.39 3.44 3.24 5.07 0.05 0.1 0.11 0.2 0.16 0.3l0.15 0.3c0.1 0.2 0.2 0.4 0.29 0.6s0.19 0.39 0.27 0.59c0.09 0.2 0.17 0.39 0.25 0.58 0.32 0.76 0.61 1.49 0.84 2.18 0.39 1.11 0.67 2.11 0.89 2.98 0.09 0.35 0.42 0.58 0.78 0.55 0.37-0.03 0.66-0.34 0.66-0.71 0.04-0.95 0.01-2.05-0.09-3.3z"/>
-</svg>
\ No newline at end of file
diff --git a/installers/charm/grafana/metadata.yaml b/installers/charm/grafana/metadata.yaml
deleted file mode 100644 (file)
index 4a74db6..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-name: osm-grafana
-summary: OSM Grafana
-description: |
-  A CAAS charm to deploy OSM's Grafana.
-series:
-  - kubernetes
-tags:
-  - kubernetes
-  - osm
-  - grafana
-min-juju-version: 2.8.0
-deployment:
-  type: stateless
-  service: cluster
-resources:
-  image:
-    type: oci-image
-    description: Ubuntu LTS image for Grafana
-    upstream-source: "ubuntu/grafana:latest"
-requires:
-  prometheus:
-    interface: prometheus
-  db:
-    interface: mysql
-    limit: 1
-peers:
-  cluster:
-    interface: grafana-cluster
diff --git a/installers/charm/grafana/requirements-test.txt b/installers/charm/grafana/requirements-test.txt
deleted file mode 100644 (file)
index cf61dd4..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-mock==4.0.3
diff --git a/installers/charm/grafana/requirements.txt b/installers/charm/grafana/requirements.txt
deleted file mode 100644 (file)
index 1a8928c..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-git+https://github.com/charmed-osm/ops-lib-charmed-osm/@master
\ No newline at end of file
diff --git a/installers/charm/grafana/src/charm.py b/installers/charm/grafana/src/charm.py
deleted file mode 100755 (executable)
index caa0277..0000000
+++ /dev/null
@@ -1,318 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-# pylint: disable=E0213
-
-from ipaddress import ip_network
-import logging
-from pathlib import Path
-import secrets
-from string import Template
-from typing import NoReturn, Optional
-from urllib.parse import urlparse
-
-from ops.main import main
-from opslib.osm.charm import CharmedOsmBase, RelationsMissing
-from opslib.osm.interfaces.grafana import GrafanaCluster
-from opslib.osm.interfaces.mysql import MysqlClient
-from opslib.osm.interfaces.prometheus import PrometheusClient
-from opslib.osm.pod import (
-    ContainerV3Builder,
-    FilesV3Builder,
-    IngressResourceV3Builder,
-    PodRestartPolicy,
-    PodSpecV3Builder,
-)
-from opslib.osm.validator import ModelValidator, validator
-
-
-logger = logging.getLogger(__name__)
-
-
-class ConfigModel(ModelValidator):
-    log_level: str
-    port: int
-    admin_user: str
-    max_file_size: int
-    osm_dashboards: bool
-    site_url: Optional[str]
-    cluster_issuer: Optional[str]
-    ingress_class: Optional[str]
-    ingress_whitelist_source_range: Optional[str]
-    tls_secret_name: Optional[str]
-    image_pull_policy: str
-    security_context: bool
-
-    @validator("log_level")
-    def validate_log_level(cls, v):
-        allowed_values = ("debug", "info", "warn", "error", "critical")
-        if v not in allowed_values:
-            separator = '", "'
-            raise ValueError(
-                f'incorrect value. Allowed values are "{separator.join(allowed_values)}"'
-            )
-        return v
-
-    @validator("max_file_size")
-    def validate_max_file_size(cls, v):
-        if v < 0:
-            raise ValueError("value must be equal or greater than 0")
-        return v
-
-    @validator("site_url")
-    def validate_site_url(cls, v):
-        if v:
-            parsed = urlparse(v)
-            if not parsed.scheme.startswith("http"):
-                raise ValueError("value must start with http")
-        return v
-
-    @validator("ingress_whitelist_source_range")
-    def validate_ingress_whitelist_source_range(cls, v):
-        if v:
-            ip_network(v)
-        return v
-
-    @validator("image_pull_policy")
-    def validate_image_pull_policy(cls, v):
-        values = {
-            "always": "Always",
-            "ifnotpresent": "IfNotPresent",
-            "never": "Never",
-        }
-        v = v.lower()
-        if v not in values.keys():
-            raise ValueError("value must be always, ifnotpresent or never")
-        return values[v]
-
-
-class GrafanaCharm(CharmedOsmBase):
-    """GrafanaCharm Charm."""
-
-    def __init__(self, *args) -> NoReturn:
-        """Prometheus Charm constructor."""
-        super().__init__(*args, oci_image="image", mysql_uri=True)
-        # Initialize relation objects
-        self.prometheus_client = PrometheusClient(self, "prometheus")
-        self.grafana_cluster = GrafanaCluster(self, "cluster")
-        self.mysql_client = MysqlClient(self, "db")
-        # Observe events
-        event_observer_mapping = {
-            self.on["prometheus"].relation_changed: self.configure_pod,
-            self.on["prometheus"].relation_broken: self.configure_pod,
-            self.on["db"].relation_changed: self.configure_pod,
-            self.on["db"].relation_broken: self.configure_pod,
-        }
-        for event, observer in event_observer_mapping.items():
-            self.framework.observe(event, observer)
-
-    def _build_dashboard_files(self, config: ConfigModel):
-        files_builder = FilesV3Builder()
-        files_builder.add_file(
-            "dashboard_osm.yaml",
-            Path("templates/default_dashboards.yaml").read_text(),
-        )
-        if config.osm_dashboards:
-            osm_dashboards_mapping = {
-                "kafka_exporter_dashboard.json": "templates/kafka_exporter_dashboard.json",
-                "mongodb_exporter_dashboard.json": "templates/mongodb_exporter_dashboard.json",
-                "mysql_exporter_dashboard.json": "templates/mysql_exporter_dashboard.json",
-                "nodes_exporter_dashboard.json": "templates/nodes_exporter_dashboard.json",
-                "summary_dashboard.json": "templates/summary_dashboard.json",
-            }
-            for file_name, path in osm_dashboards_mapping.items():
-                files_builder.add_file(file_name, Path(path).read_text())
-        return files_builder.build()
-
-    def _build_datasources_files(self):
-        files_builder = FilesV3Builder()
-        prometheus_user = self.prometheus_client.user
-        prometheus_password = self.prometheus_client.password
-        enable_basic_auth = all([prometheus_user, prometheus_password])
-        kwargs = {
-            "prometheus_host": self.prometheus_client.hostname,
-            "prometheus_port": self.prometheus_client.port,
-            "enable_basic_auth": enable_basic_auth,
-            "user": "",
-            "password": "",
-        }
-        if enable_basic_auth:
-            kwargs["user"] = f"basic_auth_user: {prometheus_user}"
-            kwargs[
-                "password"
-            ] = f"secure_json_data:\n      basicAuthPassword: {prometheus_password}"
-        files_builder.add_file(
-            "datasource_prometheus.yaml",
-            Template(Path("templates/default_datasources.yaml").read_text()).substitute(
-                **kwargs
-            ),
-        )
-        return files_builder.build()
-
-    def _check_missing_dependencies(self, config: ConfigModel, external_db: bool):
-        missing_relations = []
-
-        if self.prometheus_client.is_missing_data_in_app():
-            missing_relations.append("prometheus")
-
-        if not external_db and self.mysql_client.is_missing_data_in_unit():
-            missing_relations.append("db")
-
-        if missing_relations:
-            raise RelationsMissing(missing_relations)
-
-    def build_pod_spec(self, image_info, **kwargs):
-        # Validate config
-        config = ConfigModel(**dict(self.config))
-        mysql_config = kwargs["mysql_config"]
-        if mysql_config.mysql_uri and not self.mysql_client.is_missing_data_in_unit():
-            raise Exception("Mysql data cannot be provided via config and relation")
-
-        # Check relations
-        external_db = True if mysql_config.mysql_uri else False
-        self._check_missing_dependencies(config, external_db)
-
-        # Get initial password
-        admin_initial_password = self.grafana_cluster.admin_initial_password
-        if not admin_initial_password:
-            admin_initial_password = _generate_random_password()
-            self.grafana_cluster.set_initial_password(admin_initial_password)
-
-        # Create Builder for the PodSpec
-        pod_spec_builder = PodSpecV3Builder(
-            enable_security_context=config.security_context
-        )
-
-        # Add secrets to the pod
-        grafana_secret_name = f"{self.app.name}-admin-secret"
-        pod_spec_builder.add_secret(
-            grafana_secret_name,
-            {
-                "admin-password": admin_initial_password,
-                "mysql-url": mysql_config.mysql_uri or self.mysql_client.get_uri(),
-                "prometheus-user": self.prometheus_client.user,
-                "prometheus-password": self.prometheus_client.password,
-            },
-        )
-
-        # Build Container
-        container_builder = ContainerV3Builder(
-            self.app.name,
-            image_info,
-            config.image_pull_policy,
-            run_as_non_root=config.security_context,
-        )
-        container_builder.add_port(name=self.app.name, port=config.port)
-        container_builder.add_http_readiness_probe(
-            "/api/health",
-            config.port,
-            initial_delay_seconds=10,
-            period_seconds=10,
-            timeout_seconds=5,
-            failure_threshold=3,
-        )
-        container_builder.add_http_liveness_probe(
-            "/api/health",
-            config.port,
-            initial_delay_seconds=60,
-            timeout_seconds=30,
-            failure_threshold=10,
-        )
-        container_builder.add_volume_config(
-            "dashboards",
-            "/etc/grafana/provisioning/dashboards/",
-            self._build_dashboard_files(config),
-        )
-        container_builder.add_volume_config(
-            "datasources",
-            "/etc/grafana/provisioning/datasources/",
-            self._build_datasources_files(),
-        )
-        container_builder.add_envs(
-            {
-                "GF_SERVER_HTTP_PORT": config.port,
-                "GF_LOG_LEVEL": config.log_level,
-                "GF_SECURITY_ADMIN_USER": config.admin_user,
-            }
-        )
-        container_builder.add_secret_envs(
-            secret_name=grafana_secret_name,
-            envs={
-                "GF_SECURITY_ADMIN_PASSWORD": "admin-password",
-                "GF_DATABASE_URL": "mysql-url",
-                "PROMETHEUS_USER": "prometheus-user",
-                "PROMETHEUS_PASSWORD": "prometheus-password",
-            },
-        )
-        container = container_builder.build()
-        pod_spec_builder.add_container(container)
-
-        # Add Pod restart policy
-        restart_policy = PodRestartPolicy()
-        restart_policy.add_secrets(secret_names=(grafana_secret_name,))
-        pod_spec_builder.set_restart_policy(restart_policy)
-
-        # Add ingress resources to pod spec if site url exists
-        if config.site_url:
-            parsed = urlparse(config.site_url)
-            annotations = {
-                "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
-                    str(config.max_file_size) + "m"
-                    if config.max_file_size > 0
-                    else config.max_file_size
-                )
-            }
-            if config.ingress_class:
-                annotations["kubernetes.io/ingress.class"] = config.ingress_class
-            ingress_resource_builder = IngressResourceV3Builder(
-                f"{self.app.name}-ingress", annotations
-            )
-
-            if config.ingress_whitelist_source_range:
-                annotations[
-                    "nginx.ingress.kubernetes.io/whitelist-source-range"
-                ] = config.ingress_whitelist_source_range
-
-            if config.cluster_issuer:
-                annotations["cert-manager.io/cluster-issuer"] = config.cluster_issuer
-
-            if parsed.scheme == "https":
-                ingress_resource_builder.add_tls(
-                    [parsed.hostname], config.tls_secret_name
-                )
-            else:
-                annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
-
-            ingress_resource_builder.add_rule(
-                parsed.hostname, self.app.name, config.port
-            )
-            ingress_resource = ingress_resource_builder.build()
-            pod_spec_builder.add_ingress_resource(ingress_resource)
-        return pod_spec_builder.build()
-
-
-def _generate_random_password():
-    return secrets.token_hex(16)
-
-
-if __name__ == "__main__":
-    main(GrafanaCharm)
diff --git a/installers/charm/grafana/src/pod_spec.py b/installers/charm/grafana/src/pod_spec.py
deleted file mode 100644 (file)
index 609c466..0000000
+++ /dev/null
@@ -1,398 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-import logging
-from ipaddress import ip_network
-from typing import Any, Dict, List
-from urllib.parse import urlparse
-from pathlib import Path
-from string import Template
-
-logger = logging.getLogger(__name__)
-
-
-def _validate_max_file_size(max_file_size: int, site_url: str) -> bool:
-    """Validate max_file_size.
-
-    Args:
-        max_file_size (int): maximum file size allowed.
-        site_url (str): endpoint url.
-
-    Returns:
-        bool: True if valid, false otherwise.
-    """
-    if not site_url:
-        return True
-
-    parsed = urlparse(site_url)
-
-    if not parsed.scheme.startswith("http"):
-        return True
-
-    if max_file_size is None:
-        return False
-
-    return max_file_size >= 0
-
-
-def _validate_ip_network(network: str) -> bool:
-    """Validate IP network.
-
-    Args:
-        network (str): IP network range.
-
-    Returns:
-        bool: True if valid, false otherwise.
-    """
-    if not network:
-        return True
-
-    try:
-        ip_network(network)
-    except ValueError:
-        return False
-
-    return True
-
-
-def _validate_data(config_data: Dict[str, Any], relation_data: Dict[str, Any]) -> bool:
-    """Validates passed information.
-
-    Args:
-        config_data (Dict[str, Any]): configuration information.
-        relation_data (Dict[str, Any]): relation information
-
-    Raises:
-        ValueError: when config and/or relation data is not valid.
-    """
-    config_validators = {
-        "site_url": lambda value, _: isinstance(value, str)
-        if value is not None
-        else True,
-        "max_file_size": lambda value, values: _validate_max_file_size(
-            value, values.get("site_url")
-        ),
-        "ingress_whitelist_source_range": lambda value, _: _validate_ip_network(value),
-        "tls_secret_name": lambda value, _: isinstance(value, str)
-        if value is not None
-        else True,
-    }
-    relation_validators = {
-        "prometheus_hostname": lambda value, _: (
-            isinstance(value, str) and len(value) > 0
-        ),
-        "prometheus_port": lambda value, _: (
-            isinstance(value, str) and len(value) > 0 and int(value) > 0
-        ),
-    }
-    problems = []
-
-    for key, validator in config_validators.items():
-        valid = validator(config_data.get(key), config_data)
-
-        if not valid:
-            problems.append(key)
-
-    for key, validator in relation_validators.items():
-        valid = validator(relation_data.get(key), relation_data)
-
-        if not valid:
-            problems.append(key)
-
-    if len(problems) > 0:
-        logger.debug(relation_data)
-        raise ValueError("Errors found in: {}".format(", ".join(problems)))
-
-    return True
-
-
-def _make_pod_ports(port: int) -> List[Dict[str, Any]]:
-    """Generate pod ports details.
-
-    Args:
-        port (int): port to expose.
-
-    Returns:
-        List[Dict[str, Any]]: pod port details.
-    """
-    return [{"name": "grafana", "containerPort": port, "protocol": "TCP"}]
-
-
-def _make_pod_envconfig(
-    config: Dict[str, Any], relation_state: Dict[str, Any]
-) -> Dict[str, Any]:
-    """Generate pod environment configuration.
-
-    Args:
-        config (Dict[str, Any]): configuration information.
-        relation_state (Dict[str, Any]): relation state information.
-
-    Returns:
-        Dict[str, Any]: pod environment configuration.
-    """
-    envconfig = {}
-
-    return envconfig
-
-
-def _make_pod_ingress_resources(
-    config: Dict[str, Any], app_name: str, port: int
-) -> List[Dict[str, Any]]:
-    """Generate pod ingress resources.
-
-    Args:
-        config (Dict[str, Any]): configuration information.
-        app_name (str): application name.
-        port (int): port to expose.
-
-    Returns:
-        List[Dict[str, Any]]: pod ingress resources.
-    """
-    site_url = config.get("site_url")
-
-    if not site_url:
-        return
-
-    parsed = urlparse(site_url)
-
-    if not parsed.scheme.startswith("http"):
-        return
-
-    max_file_size = config["max_file_size"]
-    ingress_whitelist_source_range = config["ingress_whitelist_source_range"]
-
-    annotations = {
-        "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
-            str(max_file_size) + "m" if max_file_size > 0 else max_file_size
-        ),
-    }
-
-    if ingress_whitelist_source_range:
-        annotations[
-            "nginx.ingress.kubernetes.io/whitelist-source-range"
-        ] = ingress_whitelist_source_range
-
-    ingress_spec_tls = None
-
-    if parsed.scheme == "https":
-        ingress_spec_tls = [{"hosts": [parsed.hostname]}]
-        tls_secret_name = config["tls_secret_name"]
-        if tls_secret_name:
-            ingress_spec_tls[0]["secretName"] = tls_secret_name
-    else:
-        annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
-
-    ingress = {
-        "name": "{}-ingress".format(app_name),
-        "annotations": annotations,
-        "spec": {
-            "rules": [
-                {
-                    "host": parsed.hostname,
-                    "http": {
-                        "paths": [
-                            {
-                                "path": "/",
-                                "backend": {
-                                    "serviceName": app_name,
-                                    "servicePort": port,
-                                },
-                            }
-                        ]
-                    },
-                }
-            ]
-        },
-    }
-    if ingress_spec_tls:
-        ingress["spec"]["tls"] = ingress_spec_tls
-
-    return [ingress]
-
-
-def _make_pod_files(
-    config: Dict[str, Any], relation: Dict[str, Any]
-) -> List[Dict[str, Any]]:
-    """Generating ConfigMap information
-
-    Args:
-        config (Dict[str, Any]): configuration information.
-        relation (Dict[str, Any]): relation information.
-
-    Returns:
-        List[Dict[str, Any]]: ConfigMap information.
-    """
-    template_data = {**config, **relation}
-    dashboards = []
-
-    if config.get("osm_dashboards", False):
-        dashboards.extend(
-            [
-                {
-                    "path": "kafka_exporter_dashboard.json",
-                    "content": Path("files/kafka_exporter_dashboard.json").read_text(),
-                },
-                {
-                    "path": "mongodb_exporter_dashboard.json",
-                    "content": Path(
-                        "files/mongodb_exporter_dashboard.json"
-                    ).read_text(),
-                },
-                {
-                    "path": "mysql_exporter_dashboard.json",
-                    "content": Path("files/mysql_exporter_dashboard.json").read_text(),
-                },
-                {
-                    "path": "nodes_exporter_dashboard.json",
-                    "content": Path("files/nodes_exporter_dashboard.json").read_text(),
-                },
-                {
-                    "path": "summary_dashboard.json",
-                    "content": Path("files/summary_dashboard.json").read_text(),
-                },
-            ]
-        )
-
-    dashboards.append(
-        {
-            "path": "dashboard_osm.yaml",
-            "content": Path("files/default_dashboards.yaml").read_text(),
-        }
-    )
-
-    files = [
-        {
-            "name": "dashboards",
-            "mountPath": "/etc/grafana/provisioning/dashboards/",
-            "files": dashboards,
-        },
-        {
-            "name": "datasources",
-            "mountPath": "/etc/grafana/provisioning/datasources/",
-            "files": [
-                {
-                    "path": "datasource_prometheus.yaml",
-                    "content": Template(
-                        Path("files/default_dashboards.yaml").read_text()
-                    ).substitute(template_data),
-                }
-            ],
-        },
-    ]
-
-    return files
-
-
-def _make_readiness_probe(port: int) -> Dict[str, Any]:
-    """Generate readiness probe.
-
-    Args:
-        port (int): service port.
-
-    Returns:
-        Dict[str, Any]: readiness probe.
-    """
-    return {
-        "httpGet": {
-            "path": "/api/health",
-            "port": port,
-        },
-        "initialDelaySeconds": 10,
-        "periodSeconds": 10,
-        "timeoutSeconds": 5,
-        "successThreshold": 1,
-        "failureThreshold": 3,
-    }
-
-
-def _make_liveness_probe(port: int) -> Dict[str, Any]:
-    """Generate liveness probe.
-
-    Args:
-        port (int): service port.
-
-    Returns:
-        Dict[str, Any]: liveness probe.
-    """
-    return {
-        "httpGet": {
-            "path": "/api/health",
-            "port": port,
-        },
-        "initialDelaySeconds": 60,
-        "timeoutSeconds": 30,
-        "failureThreshold": 10,
-    }
-
-
-def make_pod_spec(
-    image_info: Dict[str, str],
-    config: Dict[str, Any],
-    relation_state: Dict[str, Any],
-    app_name: str = "grafana",
-    port: int = 3000,
-) -> Dict[str, Any]:
-    """Generate the pod spec information.
-
-    Args:
-        image_info (Dict[str, str]): Object provided by
-                                     OCIImageResource("image").fetch().
-        config (Dict[str, Any]): Configuration information.
-        relation_state (Dict[str, Any]): Relation state information.
-        app_name (str, optional): Application name. Defaults to "ro".
-        port (int, optional): Port for the container. Defaults to 9090.
-
-    Returns:
-        Dict[str, Any]: Pod spec dictionary for the charm.
-    """
-    if not image_info:
-        return None
-
-    _validate_data(config, relation_state)
-
-    ports = _make_pod_ports(port)
-    env_config = _make_pod_envconfig(config, relation_state)
-    files = _make_pod_files(config, relation_state)
-    readiness_probe = _make_readiness_probe(port)
-    liveness_probe = _make_liveness_probe(port)
-    ingress_resources = _make_pod_ingress_resources(config, app_name, port)
-
-    return {
-        "version": 3,
-        "containers": [
-            {
-                "name": app_name,
-                "imageDetails": image_info,
-                "imagePullPolicy": "Always",
-                "ports": ports,
-                "envConfig": env_config,
-                "volumeConfig": files,
-                "kubernetes": {
-                    "readinessProbe": readiness_probe,
-                    "livenessProbe": liveness_probe,
-                },
-            }
-        ],
-        "kubernetesResources": {
-            "ingressResources": ingress_resources or [],
-        },
-    }
diff --git a/installers/charm/grafana/templates/default_dashboards.yaml b/installers/charm/grafana/templates/default_dashboards.yaml
deleted file mode 100644 (file)
index a56ea5f..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
----
-apiVersion: 1
-providers:
-  - name: 'osm'
-    orgId: 1
-    folder: ''
-    type: file
-    options:
-      path: /etc/grafana/provisioning/dashboards/
diff --git a/installers/charm/grafana/templates/default_datasources.yaml b/installers/charm/grafana/templates/default_datasources.yaml
deleted file mode 100644 (file)
index 88e97df..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
----
-datasources:
-  - access: proxy
-    editable: true
-    is_default: true
-    name: osm_prometheus
-    orgId: 1
-    type: prometheus
-    version: 1
-    url: http://$prometheus_host:$prometheus_port
-    basic_auth: $enable_basic_auth
-    $user
-    $password
diff --git a/installers/charm/grafana/templates/kafka_exporter_dashboard.json b/installers/charm/grafana/templates/kafka_exporter_dashboard.json
deleted file mode 100644 (file)
index 5b7552a..0000000
+++ /dev/null
@@ -1,609 +0,0 @@
-{
-  "annotations": {
-    "list": [
-      {
-        "builtIn": 1,
-        "datasource": "-- Grafana --",
-        "enable": true,
-        "hide": true,
-        "iconColor": "rgba(0, 211, 255, 1)",
-        "name": "Annotations & Alerts",
-        "type": "dashboard"
-      }
-    ]
-  },
-  "description": "Kafka resource usage and throughput",
-  "editable": true,
-  "gnetId": 7589,
-  "graphTooltip": 0,
-  "id": 10,
-  "iteration": 1578848023483,
-  "links": [],
-  "panels": [
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fill": 0,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 10,
-        "w": 10,
-        "x": 0,
-        "y": 0
-      },
-      "id": 14,
-      "legend": {
-        "alignAsTable": true,
-        "avg": false,
-        "current": true,
-        "max": true,
-        "min": false,
-        "rightSide": false,
-        "show": true,
-        "sideWidth": 480,
-        "sort": "max",
-        "sortDesc": true,
-        "total": false,
-        "values": true
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "connected",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "sum(kafka_topic_partition_current_offset - kafka_topic_partition_oldest_offset{instance=\"$instance\", topic=~\"$topic\"}) by (topic)",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "{{topic}}",
-          "refId": "B"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Messages stored per topic",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": "0",
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fill": 0,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 10,
-        "w": 10,
-        "x": 10,
-        "y": 0
-      },
-      "id": 12,
-      "legend": {
-        "alignAsTable": true,
-        "avg": false,
-        "current": true,
-        "max": true,
-        "min": false,
-        "rightSide": false,
-        "show": true,
-        "sideWidth": 480,
-        "sort": "max",
-        "sortDesc": true,
-        "total": false,
-        "values": true
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "connected",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "sum(kafka_consumergroup_lag{instance=\"$instance\",topic=~\"$topic\"}) by (consumergroup, topic) ",
-          "format": "time_series",
-          "instant": false,
-          "interval": "",
-          "intervalFactor": 1,
-          "legendFormat": " {{topic}} ({{consumergroup}})",
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Lag by  Consumer Group",
-      "tooltip": {
-        "shared": true,
-        "sort": 2,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": "",
-          "logBase": 1,
-          "max": null,
-          "min": "0",
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fill": 0,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 10,
-        "w": 10,
-        "x": 0,
-        "y": 10
-      },
-      "id": 16,
-      "legend": {
-        "alignAsTable": true,
-        "avg": false,
-        "current": true,
-        "max": true,
-        "min": false,
-        "rightSide": false,
-        "show": true,
-        "sideWidth": 480,
-        "total": false,
-        "values": true
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "connected",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "sum(delta(kafka_topic_partition_current_offset{instance=~'$instance', topic=~\"$topic\"}[5m])/5) by (topic)",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "{{topic}}",
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Messages produced per minute",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fill": 0,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 10,
-        "w": 10,
-        "x": 10,
-        "y": 10
-      },
-      "id": 18,
-      "legend": {
-        "alignAsTable": true,
-        "avg": false,
-        "current": true,
-        "max": true,
-        "min": false,
-        "rightSide": false,
-        "show": true,
-        "sideWidth": 480,
-        "sort": "current",
-        "sortDesc": true,
-        "total": false,
-        "values": true
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "connected",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "sum(delta(kafka_consumergroup_current_offset{instance=~'$instance',topic=~\"$topic\"}[5m])/5) by (consumergroup, topic)",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": " {{topic}} ({{consumergroup}})",
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Messages consumed per minute",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": true,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 7,
-        "w": 20,
-        "x": 0,
-        "y": 20
-      },
-      "id": 8,
-      "legend": {
-        "alignAsTable": true,
-        "avg": false,
-        "current": true,
-        "max": false,
-        "min": false,
-        "rightSide": true,
-        "show": true,
-        "sideWidth": 420,
-        "total": false,
-        "values": true
-      },
-      "lines": false,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "sum by(topic) (kafka_topic_partitions{instance=\"$instance\",topic=~\"$topic\"})",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "{{topic}}",
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Partitions per Topic",
-      "tooltip": {
-        "shared": false,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "series",
-        "name": null,
-        "show": false,
-        "values": [
-          "current"
-        ]
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    }
-  ],
-  "refresh": "5s",
-  "schemaVersion": 19,
-  "style": "dark",
-  "tags": [],
-  "templating": {
-    "list": [
-      {
-        "allValue": null,
-        "current": {
-          "text": "osm-kafka-exporter-service",
-          "value": "osm-kafka-exporter-service"
-        },
-        "datasource": "prometheus - Juju generated source",
-        "definition": "",
-        "hide": 0,
-        "includeAll": false,
-        "label": "Job",
-        "multi": false,
-        "name": "job",
-        "options": [],
-        "query": "label_values(kafka_consumergroup_current_offset, job)",
-        "refresh": 1,
-        "regex": "",
-        "skipUrlSync": false,
-        "sort": 0,
-        "tagValuesQuery": "",
-        "tags": [],
-        "tagsQuery": "",
-        "type": "query",
-        "useTags": false
-      },
-      {
-        "allValue": null,
-        "datasource": "prometheus - Juju generated source",
-        "definition": "",
-        "hide": 0,
-        "includeAll": false,
-        "label": "Instance",
-        "multi": false,
-        "name": "instance",
-        "options": [],
-        "query": "label_values(kafka_consumergroup_current_offset{job=~\"$job\"}, instance)",
-        "refresh": 1,
-        "regex": "",
-        "skipUrlSync": false,
-        "sort": 0,
-        "tagValuesQuery": "",
-        "tags": [],
-        "tagsQuery": "",
-        "type": "query",
-        "useTags": false
-      },
-      {
-        "allValue": null,
-        "current": {
-          "tags": [],
-          "text": "All",
-          "value": [
-            "$__all"
-          ]
-        },
-        "datasource": "prometheus - Juju generated source",
-        "definition": "",
-        "hide": 0,
-        "includeAll": true,
-        "label": "Topic",
-        "multi": true,
-        "name": "topic",
-        "options": [],
-        "query": "label_values(kafka_topic_partition_current_offset{instance='$instance',topic!='__consumer_offsets',topic!='--kafka'}, topic)",
-        "refresh": 1,
-        "regex": "",
-        "skipUrlSync": false,
-        "sort": 1,
-        "tagValuesQuery": "",
-        "tags": [],
-        "tagsQuery": "topic",
-        "type": "query",
-        "useTags": false
-      }
-    ]
-  },
-  "time": {
-    "from": "now-1h",
-    "to": "now"
-  },
-  "timepicker": {
-    "refresh_intervals": [
-      "5s",
-      "10s",
-      "30s",
-      "1m",
-      "5m",
-      "15m",
-      "30m",
-      "1h",
-      "2h",
-      "1d"
-    ],
-    "time_options": [
-      "5m",
-      "15m",
-      "1h",
-      "6h",
-      "12h",
-      "24h",
-      "2d",
-      "7d",
-      "30d"
-    ]
-  },
-  "timezone": "browser",
-  "title": "Kafka",
-  "uid": "jwPKIsniz",
-  "version": 2
-}
diff --git a/installers/charm/grafana/templates/mongodb_exporter_dashboard.json b/installers/charm/grafana/templates/mongodb_exporter_dashboard.json
deleted file mode 100644 (file)
index c6c64c2..0000000
+++ /dev/null
@@ -1,938 +0,0 @@
-{
-  "annotations": {
-    "list": [
-      {
-        "builtIn": 1,
-        "datasource": "-- Grafana --",
-        "enable": true,
-        "hide": true,
-        "iconColor": "rgba(0, 211, 255, 1)",
-        "name": "Annotations & Alerts",
-        "type": "dashboard"
-      }
-    ]
-  },
-  "description": "MongoDB Prometheus Exporter Dashboard.",
-  "editable": true,
-  "gnetId": 2583,
-  "graphTooltip": 1,
-  "id": 1,
-  "iteration": 1615141074039,
-  "links": [],
-  "panels": [
-    {
-      "collapsed": false,
-      "datasource": null,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 0
-      },
-      "id": 22,
-      "panels": [],
-      "repeat": "env",
-      "title": "Health",
-      "type": "row"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": false,
-      "colorValue": true,
-      "colors": [
-        "rgba(245, 54, 54, 0.9)",
-        "rgba(237, 129, 40, 0.89)",
-        "rgba(50, 172, 45, 0.97)"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "decimals": null,
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "s",
-      "gauge": {
-        "maxValue": 100,
-        "minValue": 0,
-        "show": false,
-        "thresholdLabels": false,
-        "thresholdMarkers": true
-      },
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 0,
-        "y": 1
-      },
-      "id": 10,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": false
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "mongodb_ss_uptime{}",
-          "format": "time_series",
-          "interval": "",
-          "intervalFactor": 2,
-          "legendFormat": "",
-          "refId": "A",
-          "step": 1800
-        }
-      ],
-      "thresholds": "0,360",
-      "title": "Uptime",
-      "type": "singlestat",
-      "valueFontSize": "80%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "N/A",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": false,
-      "colorValue": false,
-      "colors": [
-        "rgba(245, 54, 54, 0.9)",
-        "rgba(237, 129, 40, 0.89)",
-        "rgba(50, 172, 45, 0.97)"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "none",
-      "gauge": {
-        "maxValue": 100,
-        "minValue": 0,
-        "show": false,
-        "thresholdLabels": false,
-        "thresholdMarkers": true
-      },
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 12,
-        "y": 1
-      },
-      "id": 1,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": true,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": true
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "mongodb_ss_connections{conn_type=\"current\"}",
-          "format": "time_series",
-          "interval": "",
-          "intervalFactor": 2,
-          "legendFormat": "",
-          "metric": "mongodb_connections",
-          "refId": "A",
-          "step": 1800
-        }
-      ],
-      "thresholds": "",
-      "title": "Open Connections",
-      "type": "singlestat",
-      "valueFontSize": "80%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "N/A",
-          "value": "null"
-        }
-      ],
-      "valueName": "avg"
-    },
-    {
-      "collapsed": false,
-      "datasource": null,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 5
-      },
-      "id": 20,
-      "panels": [],
-      "repeat": "env",
-      "title": "Operations",
-      "type": "row"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {},
-          "links": []
-        },
-        "overrides": []
-      },
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 6,
-        "w": 10,
-        "x": 0,
-        "y": 6
-      },
-      "hiddenSeries": false,
-      "id": 7,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "alertThreshold": true
-      },
-      "percentage": false,
-      "pluginVersion": "7.4.3",
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "rate(mongodb_ss_opcounters[$interval])",
-          "format": "time_series",
-          "interval": "",
-          "intervalFactor": 2,
-          "legendFormat": "{{legacy_op_type}}",
-          "refId": "A",
-          "step": 240
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Query Operations",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "$$hashKey": "object:670",
-          "format": "ops",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "$$hashKey": "object:671",
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {},
-          "links": []
-        },
-        "overrides": []
-      },
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 6,
-        "w": 8,
-        "x": 10,
-        "y": 6
-      },
-      "hiddenSeries": false,
-      "id": 9,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "alertThreshold": true
-      },
-      "percentage": false,
-      "pluginVersion": "7.4.3",
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [
-        {
-          "alias": "returned",
-          "yaxis": 1
-        }
-      ],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "rate(mongodb_ss_metrics_document[$interval])",
-          "format": "time_series",
-          "interval": "",
-          "intervalFactor": 2,
-          "legendFormat": "{{doc_op_type}}",
-          "refId": "A",
-          "step": 240
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Document Operations",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "$$hashKey": "object:699",
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "$$hashKey": "object:700",
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {},
-          "links": []
-        },
-        "overrides": []
-      },
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 6,
-        "w": 6,
-        "x": 18,
-        "y": 6
-      },
-      "hiddenSeries": false,
-      "id": 8,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "alertThreshold": true
-      },
-      "percentage": false,
-      "pluginVersion": "7.4.3",
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "rate(mongodb_ss_opcounters[$interval])",
-          "format": "time_series",
-          "interval": "",
-          "intervalFactor": 2,
-          "legendFormat": "{{legacy_op_type}}",
-          "refId": "A",
-          "step": 600
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Document Query Executor",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "$$hashKey": "object:728",
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "$$hashKey": "object:729",
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "collapsed": false,
-      "datasource": null,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 12
-      },
-      "id": 23,
-      "panels": [],
-      "repeat": null,
-      "title": "Resources",
-      "type": "row"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {},
-          "links": []
-        },
-        "overrides": []
-      },
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 6,
-        "w": 12,
-        "x": 0,
-        "y": 13
-      },
-      "hiddenSeries": false,
-      "id": 4,
-      "legend": {
-        "alignAsTable": false,
-        "avg": false,
-        "current": true,
-        "hideEmpty": false,
-        "hideZero": false,
-        "max": false,
-        "min": false,
-        "rightSide": false,
-        "show": true,
-        "total": false,
-        "values": true
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "alertThreshold": true
-      },
-      "percentage": false,
-      "pluginVersion": "7.4.3",
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "mongodb_ss_mem_resident",
-          "format": "time_series",
-          "interval": "",
-          "intervalFactor": 2,
-          "legendFormat": "Resident",
-          "refId": "A",
-          "step": 240
-        },
-        {
-          "expr": "mongodb_ss_mem_virtual",
-          "hide": false,
-          "interval": "",
-          "legendFormat": "Virtual",
-          "refId": "B"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Memory",
-      "tooltip": {
-        "shared": false,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": [
-          "total"
-        ]
-      },
-      "yaxes": [
-        {
-          "$$hashKey": "object:523",
-          "format": "decmbytes",
-          "label": "",
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "$$hashKey": "object:524",
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {},
-          "links": []
-        },
-        "overrides": []
-      },
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 6,
-        "w": 12,
-        "x": 12,
-        "y": 13
-      },
-      "hiddenSeries": false,
-      "id": 5,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "alertThreshold": true
-      },
-      "percentage": false,
-      "pluginVersion": "7.4.3",
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "rate(mongodb_ss_network_bytesOut[$interval])",
-          "format": "time_series",
-          "interval": "",
-          "intervalFactor": 2,
-          "legendFormat": "Out",
-          "metric": "mongodb_metrics_operation_total",
-          "refId": "A",
-          "step": 240
-        },
-        {
-          "expr": "rate(mongodb_ss_network_bytesIn[$interval])",
-          "hide": false,
-          "interval": "",
-          "intervalFactor": 2,
-          "legendFormat": "In",
-          "refId": "B"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Network I/O",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "$$hashKey": "object:579",
-          "format": "decbytes",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "$$hashKey": "object:580",
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    }
-  ],
-  "refresh": "5s",
-  "schemaVersion": 27,
-  "style": "dark",
-  "tags": [],
-  "templating": {
-    "list": [
-      {
-        "allValue": null,
-        "current": {
-          "selected": true,
-          "text": [
-            "All"
-          ],
-          "value": [
-            "$__all"
-          ]
-        },
-        "datasource": "prometheus - Juju generated source",
-        "definition": "",
-        "description": null,
-        "error": null,
-        "hide": 0,
-        "includeAll": true,
-        "label": "instance",
-        "multi": true,
-        "name": "instance",
-        "options": [],
-        "query": {
-          "query": "label_values(mongodb_connections, instance)",
-          "refId": "prometheus - Juju generated source-instance-Variable-Query"
-        },
-        "refresh": 1,
-        "regex": "",
-        "skipUrlSync": false,
-        "sort": 1,
-        "tagValuesQuery": "/.*-(.*?)-.*/",
-        "tags": [],
-        "tagsQuery": "label_values(mongodb_connections, instance)",
-        "type": "query",
-        "useTags": false
-      },
-      {
-        "auto": true,
-        "auto_count": 30,
-        "auto_min": "10s",
-        "current": {
-          "selected": false,
-          "text": "auto",
-          "value": "$__auto_interval_interval"
-        },
-        "description": null,
-        "error": null,
-        "hide": 0,
-        "label": null,
-        "name": "interval",
-        "options": [
-          {
-            "selected": true,
-            "text": "auto",
-            "value": "$__auto_interval_interval"
-          },
-          {
-            "selected": false,
-            "text": "1m",
-            "value": "1m"
-          },
-          {
-            "selected": false,
-            "text": "10m",
-            "value": "10m"
-          },
-          {
-            "selected": false,
-            "text": "30m",
-            "value": "30m"
-          },
-          {
-            "selected": false,
-            "text": "1h",
-            "value": "1h"
-          },
-          {
-            "selected": false,
-            "text": "6h",
-            "value": "6h"
-          },
-          {
-            "selected": false,
-            "text": "12h",
-            "value": "12h"
-          },
-          {
-            "selected": false,
-            "text": "1d",
-            "value": "1d"
-          },
-          {
-            "selected": false,
-            "text": "7d",
-            "value": "7d"
-          },
-          {
-            "selected": false,
-            "text": "14d",
-            "value": "14d"
-          },
-          {
-            "selected": false,
-            "text": "30d",
-            "value": "30d"
-          }
-        ],
-        "query": "1m,10m,30m,1h,6h,12h,1d,7d,14d,30d",
-        "refresh": 2,
-        "skipUrlSync": false,
-        "type": "interval"
-      }
-    ]
-  },
-  "time": {
-    "from": "now/d",
-    "to": "now"
-  },
-  "timepicker": {
-    "refresh_intervals": [
-      "5s",
-      "10s",
-      "30s",
-      "1m",
-      "5m",
-      "15m",
-      "30m",
-      "1h",
-      "2h",
-      "1d"
-    ],
-    "time_options": [
-      "5m",
-      "15m",
-      "1h",
-      "6h",
-      "12h",
-      "24h",
-      "2d",
-      "7d",
-      "30d"
-    ]
-  },
-  "timezone": "browser",
-  "title": "MongoDB",
-  "uid": "HEK4NbtZk",
-  "version": 17
-}
\ No newline at end of file
diff --git a/installers/charm/grafana/templates/mysql_exporter_dashboard.json b/installers/charm/grafana/templates/mysql_exporter_dashboard.json
deleted file mode 100644 (file)
index 9f9acac..0000000
+++ /dev/null
@@ -1,1145 +0,0 @@
-{
-  "annotations": {
-    "list": [
-      {
-        "builtIn": 1,
-        "datasource": "-- Grafana --",
-        "enable": true,
-        "hide": true,
-        "iconColor": "rgba(0, 211, 255, 1)",
-        "name": "Annotations & Alerts",
-        "type": "dashboard"
-      }
-    ]
-  },
-  "description": "Mysql dashboard",
-  "editable": true,
-  "gnetId": 6239,
-  "graphTooltip": 0,
-  "id": 34,
-  "iteration": 1569307668513,
-  "links": [],
-  "panels": [
-    {
-      "collapsed": false,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 0
-      },
-      "id": 17,
-      "panels": [],
-      "title": "Global status",
-      "type": "row"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": true,
-      "colorValue": false,
-      "colors": [
-        "#bf1b00",
-        "#508642",
-        "#ef843c"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "format": "none",
-      "gauge": {
-        "maxValue": 1,
-        "minValue": 0,
-        "show": false,
-        "thresholdLabels": false,
-        "thresholdMarkers": true
-      },
-      "gridPos": {
-        "h": 7,
-        "w": 6,
-        "x": 0,
-        "y": 1
-      },
-      "id": 11,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "options": {},
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": true,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": true
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "mysql_up{release=\"$release\"}",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": "1,2",
-      "title": "Instance Up",
-      "type": "singlestat",
-      "valueFontSize": "80%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "N/A",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": true,
-      "colorValue": false,
-      "colors": [
-        "#d44a3a",
-        "rgba(237, 129, 40, 0.89)",
-        "#508642"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "format": "s",
-      "gauge": {
-        "maxValue": 100,
-        "minValue": 0,
-        "show": false,
-        "thresholdLabels": false,
-        "thresholdMarkers": true
-      },
-      "gridPos": {
-        "h": 7,
-        "w": 6,
-        "x": 6,
-        "y": 1
-      },
-      "id": 15,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "options": {},
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": true
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "mysql_global_status_uptime{release=\"$release\"}",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": "25200,32400",
-      "title": "Uptime",
-      "type": "singlestat",
-      "valueFontSize": "80%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "N/A",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 7,
-        "w": 12,
-        "x": 12,
-        "y": 1
-      },
-      "id": 29,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": false,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "mysql_global_status_max_used_connections{release=\"$release\"}",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "current",
-          "refId": "A"
-        },
-        {
-          "expr": "mysql_global_variables_max_connections{release=\"$release\"}",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "Max",
-          "refId": "B"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Mysql Connections",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "collapsed": false,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 8
-      },
-      "id": 19,
-      "panels": [],
-      "title": "I/O",
-      "type": "row"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 9,
-        "w": 12,
-        "x": 0,
-        "y": 9
-      },
-      "id": 5,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [
-        {
-          "alias": "write",
-          "transform": "negative-Y"
-        }
-      ],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "irate(mysql_global_status_innodb_data_reads{release=\"$release\"}[10m])",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "reads",
-          "refId": "A"
-        },
-        {
-          "expr": "irate(mysql_global_status_innodb_data_writes{release=\"$release\"}[10m])",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "write",
-          "refId": "B"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "mysql  disk reads vs writes",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 9,
-        "w": 12,
-        "x": 12,
-        "y": 9
-      },
-      "id": 9,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": false,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [
-        {
-          "alias": "/sent/",
-          "transform": "negative-Y"
-        }
-      ],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "irate(mysql_global_status_bytes_received{release=\"$release\"}[5m])",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "received",
-          "refId": "A"
-        },
-        {
-          "expr": "irate(mysql_global_status_bytes_sent{release=\"$release\"}[5m])",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "sent",
-          "refId": "B"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "mysql network received vs sent",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 7,
-        "w": 12,
-        "x": 0,
-        "y": 18
-      },
-      "id": 2,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": false,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "irate(mysql_global_status_commands_total{release=\"$release\"}[5m]) > 0",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "{{ command }} - {{ release }}",
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Query rates",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 7,
-        "w": 12,
-        "x": 12,
-        "y": 18
-      },
-      "id": 25,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": false,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "mysql_global_status_threads_running{release=\"$release\"} ",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Running Threads",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "decimals": null,
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": "15",
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "collapsed": false,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 25
-      },
-      "id": 21,
-      "panels": [],
-      "title": "Errors",
-      "type": "row"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "description": "The number of connections that were aborted because the client died without closing the connection properly.",
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 9,
-        "w": 12,
-        "x": 0,
-        "y": 26
-      },
-      "id": 13,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": false,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "mysql_global_status_aborted_clients{release=\"$release\"}",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "B"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Aborted clients",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "description": "The number of failed attempts to connect to the MySQL server.",
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 9,
-        "w": 12,
-        "x": 12,
-        "y": 26
-      },
-      "id": 4,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": false,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "mysql_global_status_aborted_connects{release=\"$release\"}",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "",
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "mysql aborted Connects",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "collapsed": false,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 35
-      },
-      "id": 23,
-      "panels": [],
-      "title": "Disk usage",
-      "type": "row"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 9,
-        "w": 12,
-        "x": 0,
-        "y": 36
-      },
-      "id": 27,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "sum(mysql_info_schema_table_size{component=\"data_length\",release=\"$release\"})",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "Tables",
-          "refId": "A"
-        },
-        {
-          "expr": "sum(mysql_info_schema_table_size{component=\"index_length\",release=\"$release\"})",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "Indexes",
-          "refId": "B"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Disk usage tables / indexes",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "decbytes",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 9,
-        "w": 12,
-        "x": 12,
-        "y": 36
-      },
-      "id": 7,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": false,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "sum(mysql_info_schema_table_rows{release=\"$release\"})",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Sum of all rows",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "decimals": null,
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    }
-  ],
-  "schemaVersion": 19,
-  "style": "dark",
-  "tags": [
-  ],
-  "templating": {
-    "list": [
-      {
-        "allValue": null,
-        "current": {
-          "isNone": true,
-          "text": "None",
-          "value": ""
-        },
-        "datasource": "prometheus - Juju generated source",
-        "definition": "",
-        "hide": 0,
-        "includeAll": false,
-        "label": null,
-        "multi": false,
-        "name": "release",
-        "options": [],
-        "query": "label_values(mysql_up,release)",
-        "refresh": 1,
-        "regex": "",
-        "skipUrlSync": false,
-        "sort": 0,
-        "tagValuesQuery": "",
-        "tags": [],
-        "tagsQuery": "",
-        "type": "query",
-        "useTags": false
-      }
-    ]
-  },
-  "time": {
-    "from": "now-1h",
-    "to": "now"
-  },
-  "timepicker": {
-    "refresh_intervals": [
-      "5s",
-      "10s",
-      "30s",
-      "1m",
-      "5m",
-      "15m",
-      "30m",
-      "1h",
-      "2h",
-      "1d"
-    ],
-    "time_options": [
-      "5m",
-      "15m",
-      "1h",
-      "6h",
-      "12h",
-      "24h",
-      "2d",
-      "7d",
-      "30d"
-    ]
-  },
-  "timezone": "",
-  "title": "Mysql",
-  "uid": "6-kPlS7ik",
-  "version": 1
-}
diff --git a/installers/charm/grafana/templates/nodes_exporter_dashboard.json b/installers/charm/grafana/templates/nodes_exporter_dashboard.json
deleted file mode 100644 (file)
index c67f203..0000000
+++ /dev/null
@@ -1,1965 +0,0 @@
-{
-  "annotations": {
-    "list": [
-      {
-        "builtIn": 1,
-        "datasource": "-- Grafana --",
-        "enable": true,
-        "hide": true,
-        "iconColor": "rgba(0, 211, 255, 1)",
-        "name": "Annotations & Alerts",
-        "type": "dashboard"
-      }
-    ]
-  },
-  "description": "Physical nodes dashboard",
-  "editable": true,
-  "gnetId": 11074,
-  "graphTooltip": 0,
-  "id": 4,
-  "iteration": 1615160452938,
-  "links": [],
-  "panels": [
-    {
-      "collapsed": false,
-      "datasource": null,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 0
-      },
-      "id": 179,
-      "panels": [],
-      "title": "Summary",
-      "type": "row"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": false,
-      "colorPostfix": false,
-      "colorPrefix": false,
-      "colorValue": true,
-      "colors": [
-        "rgba(245, 54, 54, 0.9)",
-        "rgba(237, 129, 40, 0.89)",
-        "rgba(50, 172, 45, 0.97)"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "decimals": 1,
-      "description": "",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "s",
-      "gauge": {
-        "maxValue": 100,
-        "minValue": 0,
-        "show": false,
-        "thresholdLabels": false,
-        "thresholdMarkers": true
-      },
-      "gridPos": {
-        "h": 6,
-        "w": 2,
-        "x": 0,
-        "y": 1
-      },
-      "hideTimeOverride": true,
-      "id": 15,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "null",
-      "nullText": null,
-      "pluginVersion": "6.4.2",
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": false
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "max(system_uptime)",
-          "format": "time_series",
-          "hide": false,
-          "instant": true,
-          "interval": "",
-          "intervalFactor": 1,
-          "legendFormat": "",
-          "refId": "A",
-          "step": 40
-        }
-      ],
-      "thresholds": "1,2",
-      "title": "System Uptime",
-      "type": "singlestat",
-      "valueFontSize": "70%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "N/A",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": false,
-      "colorPostfix": false,
-      "colorValue": true,
-      "colors": [
-        "rgba(245, 54, 54, 0.9)",
-        "rgba(237, 129, 40, 0.89)",
-        "rgba(50, 172, 45, 0.97)"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "description": "",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "short",
-      "gauge": {
-        "maxValue": 100,
-        "minValue": 0,
-        "show": false,
-        "thresholdLabels": false,
-        "thresholdMarkers": true
-      },
-      "gridPos": {
-        "h": 6,
-        "w": 2,
-        "x": 2,
-        "y": 1
-      },
-      "id": 14,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "maxPerRow": 6,
-      "nullPointMode": "null",
-      "nullText": null,
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": false
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "sum(system_n_cpus)",
-          "format": "time_series",
-          "instant": true,
-          "interval": "",
-          "intervalFactor": 1,
-          "legendFormat": "",
-          "refId": "A",
-          "step": 20
-        }
-      ],
-      "thresholds": "1,2",
-      "title": "CPU Cores",
-      "type": "singlestat",
-      "valueFontSize": "70%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "N/A",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": false,
-      "colorValue": true,
-      "colors": [
-        "rgba(245, 54, 54, 0.9)",
-        "rgba(237, 129, 40, 0.89)",
-        "rgba(50, 172, 45, 0.97)"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "decimals": 2,
-      "description": "",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "bytes",
-      "gauge": {
-        "maxValue": 100,
-        "minValue": 0,
-        "show": false,
-        "thresholdLabels": false,
-        "thresholdMarkers": true
-      },
-      "gridPos": {
-        "h": 6,
-        "w": 2,
-        "x": 4,
-        "y": 1
-      },
-      "id": 75,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "maxPerRow": 6,
-      "nullPointMode": "null",
-      "nullText": null,
-      "postfix": "",
-      "postfixFontSize": "70%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": false
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "sum(mem_total)",
-          "format": "time_series",
-          "instant": true,
-          "interval": "",
-          "intervalFactor": 1,
-          "legendFormat": "",
-          "refId": "A",
-          "step": 20
-        }
-      ],
-      "thresholds": "2,3",
-      "title": "Total RAM",
-      "type": "singlestat",
-      "valueFontSize": "70%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "N/A",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "color": {
-            "mode": "thresholds"
-          },
-          "custom": {},
-          "displayName": "",
-          "mappings": [
-            {
-              "from": "",
-              "id": 1,
-              "operator": "",
-              "text": "-",
-              "to": "",
-              "type": 1,
-              "value": "NaN"
-            }
-          ],
-          "max": 100,
-          "min": 0,
-          "thresholds": {
-            "mode": "absolute",
-            "steps": [
-              {
-                "color": "green",
-                "value": null
-              },
-              {
-                "color": "#EAB839",
-                "value": 60
-              },
-              {
-                "color": "red",
-                "value": 80
-              }
-            ]
-          },
-          "unit": "percent"
-        },
-        "overrides": []
-      },
-      "gridPos": {
-        "h": 6,
-        "w": 18,
-        "x": 6,
-        "y": 1
-      },
-      "id": 177,
-      "options": {
-        "displayMode": "lcd",
-        "orientation": "horizontal",
-        "reduceOptions": {
-          "calcs": [
-            "last"
-          ],
-          "fields": "",
-          "values": false
-        },
-        "showUnfilled": true,
-        "text": {}
-      },
-      "pluginVersion": "7.4.3",
-      "targets": [
-        {
-          "expr": "avg(irate(cpu_usage_system[$__rate_interval]) + irate(cpu_usage_user[$__rate_interval])) * 100",
-          "hide": false,
-          "instant": true,
-          "interval": "",
-          "legendFormat": "CPU Busy",
-          "refId": "A"
-        },
-        {
-          "expr": "avg(irate(cpu_usage_iowait[$__rate_interval])) * 100",
-          "hide": false,
-          "instant": true,
-          "interval": "",
-          "legendFormat": "Busy Iowait",
-          "refId": "C"
-        },
-        {
-          "expr": "avg(mem_used_percent)",
-          "instant": true,
-          "interval": "",
-          "legendFormat": "Used RAM Memory",
-          "refId": "B"
-        },
-        {
-          "expr": "avg(disk_used_percent{fstype=\"ext4\"})",
-          "hide": false,
-          "instant": true,
-          "interval": "",
-          "legendFormat": "Used Max Mount($maxmount)",
-          "refId": "D"
-        },
-        {
-          "expr": "avg(swap_used_percent)",
-          "instant": true,
-          "interval": "",
-          "legendFormat": "Used SWAP",
-          "refId": "E"
-        }
-      ],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "",
-      "type": "bargauge"
-    },
-    {
-      "collapsed": true,
-      "datasource": null,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 7
-      },
-      "id": 181,
-      "panels": [
-        {
-          "aliasColors": {
-            "15分钟": "#6ED0E0",
-            "1分钟": "#BF1B00",
-            "5分钟": "#CCA300"
-          },
-          "bars": false,
-          "dashLength": 10,
-          "dashes": false,
-          "datasource": "prometheus - Juju generated source",
-          "editable": true,
-          "error": false,
-          "fieldConfig": {
-            "defaults": {
-              "custom": {},
-              "links": []
-            },
-            "overrides": []
-          },
-          "fill": 1,
-          "fillGradient": 1,
-          "grid": {},
-          "gridPos": {
-            "h": 10,
-            "w": 12,
-            "x": 0,
-            "y": 2
-          },
-          "height": "300",
-          "hiddenSeries": false,
-          "id": 13,
-          "legend": {
-            "alignAsTable": true,
-            "avg": true,
-            "current": true,
-            "max": true,
-            "min": false,
-            "rightSide": false,
-            "show": true,
-            "total": false,
-            "values": true
-          },
-          "lines": true,
-          "linewidth": 2,
-          "links": [],
-          "maxPerRow": 6,
-          "nullPointMode": "null as zero",
-          "options": {
-            "alertThreshold": true
-          },
-          "percentage": false,
-          "pluginVersion": "7.4.3",
-          "pointradius": 5,
-          "points": false,
-          "renderer": "flot",
-          "repeat": null,
-          "seriesOverrides": [],
-          "spaceLength": 10,
-          "stack": false,
-          "steppedLine": false,
-          "targets": [
-            {
-              "expr": "system_load1",
-              "format": "time_series",
-              "instant": false,
-              "interval": "",
-              "intervalFactor": 1,
-              "legendFormat": "{{juju_unit}}_1m",
-              "metric": "",
-              "refId": "A",
-              "step": 20,
-              "target": ""
-            },
-            {
-              "expr": "system_load5",
-              "format": "time_series",
-              "instant": false,
-              "interval": "",
-              "intervalFactor": 1,
-              "legendFormat": "{{juju_unit}}_5m",
-              "refId": "B",
-              "step": 20
-            },
-            {
-              "expr": "system_load15",
-              "format": "time_series",
-              "instant": false,
-              "interval": "",
-              "intervalFactor": 1,
-              "legendFormat": "{{juju_unit}}_15m",
-              "refId": "C",
-              "step": 20
-            }
-          ],
-          "thresholds": [],
-          "timeFrom": null,
-          "timeRegions": [],
-          "timeShift": null,
-          "title": "System Load",
-          "tooltip": {
-            "msResolution": false,
-            "shared": true,
-            "sort": 2,
-            "value_type": "cumulative"
-          },
-          "type": "graph",
-          "xaxis": {
-            "buckets": null,
-            "mode": "time",
-            "name": null,
-            "show": true,
-            "values": []
-          },
-          "yaxes": [
-            {
-              "format": "short",
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            },
-            {
-              "format": "short",
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            }
-          ],
-          "yaxis": {
-            "align": false,
-            "alignLevel": null
-          }
-        },
-        {
-          "aliasColors": {
-            "192.168.200.241:9100_Total": "dark-red",
-            "Idle - Waiting for something to happen": "#052B51",
-            "guest": "#9AC48A",
-            "idle": "#052B51",
-            "iowait": "#EAB839",
-            "irq": "#BF1B00",
-            "nice": "#C15C17",
-            "softirq": "#E24D42",
-            "steal": "#FCE2DE",
-            "system": "#508642",
-            "user": "#5195CE"
-          },
-          "bars": false,
-          "dashLength": 10,
-          "dashes": false,
-          "datasource": "prometheus - Juju generated source",
-          "decimals": 2,
-          "description": "",
-          "fieldConfig": {
-            "defaults": {
-              "custom": {},
-              "links": []
-            },
-            "overrides": []
-          },
-          "fill": 1,
-          "fillGradient": 0,
-          "gridPos": {
-            "h": 10,
-            "w": 12,
-            "x": 12,
-            "y": 2
-          },
-          "hiddenSeries": false,
-          "id": 7,
-          "legend": {
-            "alignAsTable": true,
-            "avg": true,
-            "current": true,
-            "hideEmpty": true,
-            "hideZero": true,
-            "max": true,
-            "min": false,
-            "rightSide": false,
-            "show": true,
-            "sideWidth": null,
-            "sort": "current",
-            "sortDesc": true,
-            "total": false,
-            "values": true
-          },
-          "lines": true,
-          "linewidth": 2,
-          "links": [],
-          "maxPerRow": 6,
-          "nullPointMode": "null",
-          "options": {
-            "alertThreshold": true
-          },
-          "percentage": false,
-          "pluginVersion": "7.4.3",
-          "pointradius": 5,
-          "points": false,
-          "renderer": "flot",
-          "repeat": null,
-          "seriesOverrides": [
-            {
-              "alias": "/.*_Total/",
-              "color": "#C4162A",
-              "fill": 0
-            }
-          ],
-          "spaceLength": 10,
-          "stack": false,
-          "steppedLine": false,
-          "targets": [
-            {
-              "expr": "avg(irate(cpu_usage_system[30m])) by (juju_unit)",
-              "format": "time_series",
-              "hide": false,
-              "instant": false,
-              "interval": "",
-              "intervalFactor": 1,
-              "legendFormat": "{{juju_unit}}_System",
-              "refId": "A",
-              "step": 20
-            },
-            {
-              "expr": "avg(irate(cpu_usage_user[30m])) by (juju_unit)",
-              "format": "time_series",
-              "hide": false,
-              "intervalFactor": 1,
-              "legendFormat": "{{juju_unit}}_User",
-              "refId": "B",
-              "step": 240
-            },
-            {
-              "expr": "avg(irate(cpu_usage_iowait[30m])) by (juju_unit)",
-              "format": "time_series",
-              "hide": false,
-              "instant": false,
-              "intervalFactor": 1,
-              "legendFormat": "{{juju_unit}}_Iowait",
-              "refId": "D",
-              "step": 240
-            },
-            {
-              "expr": "1 - avg(irate(cpu_usage_idle[30m])) by (juju_unit)",
-              "format": "time_series",
-              "hide": false,
-              "intervalFactor": 1,
-              "legendFormat": "{{juju_unit}}_Total",
-              "refId": "F",
-              "step": 240
-            }
-          ],
-          "thresholds": [],
-          "timeFrom": null,
-          "timeRegions": [],
-          "timeShift": null,
-          "title": "CPU",
-          "tooltip": {
-            "shared": true,
-            "sort": 2,
-            "value_type": "individual"
-          },
-          "type": "graph",
-          "xaxis": {
-            "buckets": null,
-            "mode": "time",
-            "name": null,
-            "show": true,
-            "values": []
-          },
-          "yaxes": [
-            {
-              "decimals": 2,
-              "format": "percentunit",
-              "label": "",
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            },
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": false
-            }
-          ],
-          "yaxis": {
-            "align": false,
-            "alignLevel": null
-          }
-        }
-      ],
-      "title": "CPU",
-      "type": "row"
-    },
-    {
-      "collapsed": true,
-      "datasource": null,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 8
-      },
-      "id": 183,
-      "panels": [
-        {
-          "datasource": "prometheus - Juju generated source",
-          "fieldConfig": {
-            "defaults": {
-              "color": {
-                "mode": "thresholds"
-              },
-              "custom": {
-                "align": null,
-                "filterable": false
-              },
-              "decimals": 2,
-              "displayName": "",
-              "mappings": [],
-              "thresholds": {
-                "mode": "percentage",
-                "steps": [
-                  {
-                    "color": "green",
-                    "value": null
-                  },
-                  {
-                    "color": "red",
-                    "value": 80
-                  }
-                ]
-              },
-              "unit": "short"
-            },
-            "overrides": [
-              {
-                "matcher": {
-                  "id": "byName",
-                  "options": "juju_unit"
-                },
-                "properties": [
-                  {
-                    "id": "displayName",
-                    "value": "Unit"
-                  }
-                ]
-              },
-              {
-                "matcher": {
-                  "id": "byName",
-                  "options": "device (lastNotNull)"
-                },
-                "properties": [
-                  {
-                    "id": "displayName",
-                    "value": "Device"
-                  }
-                ]
-              },
-              {
-                "matcher": {
-                  "id": "byName",
-                  "options": "fstype (lastNotNull)"
-                },
-                "properties": [
-                  {
-                    "id": "displayName",
-                    "value": "Filesystem"
-                  }
-                ]
-              },
-              {
-                "matcher": {
-                  "id": "byName",
-                  "options": "path (lastNotNull)"
-                },
-                "properties": [
-                  {
-                    "id": "displayName",
-                    "value": "Mounted on"
-                  }
-                ]
-              },
-              {
-                "matcher": {
-                  "id": "byName",
-                  "options": "Value #A (lastNotNull)"
-                },
-                "properties": [
-                  {
-                    "id": "displayName",
-                    "value": "Avail"
-                  },
-                  {
-                    "id": "unit",
-                    "value": "decbytes"
-                  }
-                ]
-              },
-              {
-                "matcher": {
-                  "id": "byName",
-                  "options": "Value #B (lastNotNull)"
-                },
-                "properties": [
-                  {
-                    "id": "displayName",
-                    "value": "Used"
-                  },
-                  {
-                    "id": "unit",
-                    "value": "percent"
-                  },
-                  {
-                    "id": "custom.displayMode",
-                    "value": "color-background"
-                  }
-                ]
-              },
-              {
-                "matcher": {
-                  "id": "byName",
-                  "options": "Value #C (lastNotNull)"
-                },
-                "properties": [
-                  {
-                    "id": "displayName",
-                    "value": "Size"
-                  },
-                  {
-                    "id": "unit",
-                    "value": "decbytes"
-                  }
-                ]
-              },
-              {
-                "matcher": {
-                  "id": "byName",
-                  "options": "Device"
-                },
-                "properties": [
-                  {
-                    "id": "custom.width",
-                    "value": 101
-                  }
-                ]
-              },
-              {
-                "matcher": {
-                  "id": "byName",
-                  "options": "Filesystem"
-                },
-                "properties": [
-                  {
-                    "id": "custom.width",
-                    "value": 86
-                  }
-                ]
-              },
-              {
-                "matcher": {
-                  "id": "byName",
-                  "options": "Unit"
-                },
-                "properties": [
-                  {
-                    "id": "custom.width",
-                    "value": 151
-                  }
-                ]
-              },
-              {
-                "matcher": {
-                  "id": "byName",
-                  "options": "Mounted on"
-                },
-                "properties": [
-                  {
-                    "id": "custom.width",
-                    "value": 94
-                  }
-                ]
-              },
-              {
-                "matcher": {
-                  "id": "byName",
-                  "options": "Avail"
-                },
-                "properties": [
-                  {
-                    "id": "custom.width",
-                    "value": 74
-                  }
-                ]
-              },
-              {
-                "matcher": {
-                  "id": "byName",
-                  "options": "Used"
-                },
-                "properties": [
-                  {
-                    "id": "custom.width",
-                    "value": 64
-                  }
-                ]
-              },
-              {
-                "matcher": {
-                  "id": "byName",
-                  "options": "Size"
-                },
-                "properties": [
-                  {
-                    "id": "custom.width",
-                    "value": 86
-                  }
-                ]
-              }
-            ]
-          },
-          "gridPos": {
-            "h": 8,
-            "w": 8,
-            "x": 0,
-            "y": 3
-          },
-          "id": 164,
-          "links": [],
-          "options": {
-            "showHeader": true,
-            "sortBy": []
-          },
-          "pluginVersion": "7.4.3",
-          "targets": [
-            {
-              "expr": "disk_free{fstype=\"ext4\"}",
-              "format": "table",
-              "hide": false,
-              "instant": true,
-              "interval": "10s",
-              "intervalFactor": 1,
-              "legendFormat": "",
-              "refId": "A"
-            },
-            {
-              "expr": "disk_used_percent{fstype=\"ext4\"}",
-              "format": "table",
-              "hide": false,
-              "instant": true,
-              "interval": "",
-              "intervalFactor": 1,
-              "legendFormat": "",
-              "refId": "B"
-            },
-            {
-              "expr": "disk_total{fstype=\"ext4\"}",
-              "format": "table",
-              "hide": false,
-              "instant": true,
-              "intervalFactor": 1,
-              "legendFormat": "",
-              "refId": "C"
-            }
-          ],
-          "title": "Disk Space Used  (EXT4/XFS)",
-          "transformations": [
-            {
-              "id": "merge",
-              "options": {}
-            },
-            {
-              "id": "groupBy",
-              "options": {
-                "fields": {
-                  "Value #A": {
-                    "aggregations": [
-                      "lastNotNull"
-                    ],
-                    "operation": "aggregate"
-                  },
-                  "Value #B": {
-                    "aggregations": [
-                      "lastNotNull"
-                    ],
-                    "operation": "aggregate"
-                  },
-                  "Value #C": {
-                    "aggregations": [
-                      "lastNotNull"
-                    ],
-                    "operation": "aggregate"
-                  },
-                  "device": {
-                    "aggregations": [
-                      "lastNotNull"
-                    ],
-                    "operation": "aggregate"
-                  },
-                  "fstype": {
-                    "aggregations": [
-                      "lastNotNull"
-                    ],
-                    "operation": "aggregate"
-                  },
-                  "juju_unit": {
-                    "aggregations": [],
-                    "operation": "groupby"
-                  },
-                  "path": {
-                    "aggregations": [
-                      "lastNotNull"
-                    ],
-                    "operation": "aggregate"
-                  }
-                }
-              }
-            }
-          ],
-          "type": "table"
-        },
-        {
-          "datasource": "prometheus - Juju generated source",
-          "description": "Per second read / write bytes ",
-          "fieldConfig": {
-            "defaults": {
-              "color": {
-                "mode": "palette-classic"
-              },
-              "custom": {
-                "axisLabel": "Bytes read (-) / write (+)",
-                "axisPlacement": "auto",
-                "barAlignment": 0,
-                "drawStyle": "line",
-                "fillOpacity": 10,
-                "gradientMode": "opacity",
-                "hideFrom": {
-                  "graph": false,
-                  "legend": false,
-                  "tooltip": false
-                },
-                "lineInterpolation": "linear",
-                "lineWidth": 2,
-                "pointSize": 5,
-                "scaleDistribution": {
-                  "type": "linear"
-                },
-                "showPoints": "never",
-                "spanNulls": true
-              },
-              "mappings": [],
-              "thresholds": {
-                "mode": "absolute",
-                "steps": [
-                  {
-                    "color": "green",
-                    "value": null
-                  }
-                ]
-              },
-              "unit": "Bps"
-            },
-            "overrides": []
-          },
-          "gridPos": {
-            "h": 8,
-            "w": 8,
-            "x": 8,
-            "y": 3
-          },
-          "id": 168,
-          "links": [],
-          "options": {
-            "graph": {},
-            "legend": {
-              "calcs": [
-                "mean",
-                "max",
-                "min",
-                "sum"
-              ],
-              "displayMode": "table",
-              "placement": "bottom"
-            },
-            "tooltipOptions": {
-              "mode": "single"
-            }
-          },
-          "pluginVersion": "7.4.3",
-          "targets": [
-            {
-              "expr": "rate(diskio_read_bytes{name!~\"loop.*\"}[$__rate_interval])",
-              "format": "time_series",
-              "hide": false,
-              "interval": "",
-              "intervalFactor": 1,
-              "legendFormat": "{{juju_unit}}_{{name}}_Read bytes",
-              "refId": "A",
-              "step": 10
-            },
-            {
-              "expr": "irate(diskio_write_bytes{name!~\"loop.*\"}[$__rate_interval])",
-              "format": "time_series",
-              "hide": false,
-              "interval": "",
-              "intervalFactor": 1,
-              "legendFormat": "{{juju_unit}}_{{name}}_Written bytes",
-              "refId": "B",
-              "step": 10
-            }
-          ],
-          "timeFrom": null,
-          "timeShift": null,
-          "title": "Disk R/W Data",
-          "type": "timeseries"
-        },
-        {
-          "aliasColors": {
-            "Idle - Waiting for something to happen": "#052B51",
-            "guest": "#9AC48A",
-            "idle": "#052B51",
-            "iowait": "#EAB839",
-            "irq": "#BF1B00",
-            "nice": "#C15C17",
-            "sdb_每秒I/O操作%": "#d683ce",
-            "softirq": "#E24D42",
-            "steal": "#FCE2DE",
-            "system": "#508642",
-            "user": "#5195CE",
-            "磁盘花费在I/O操作占比": "#ba43a9"
-          },
-          "bars": false,
-          "dashLength": 10,
-          "dashes": false,
-          "datasource": "prometheus - Juju generated source",
-          "decimals": null,
-          "description": "The time spent on I/O in the natural time of each second.(wall-clock time)",
-          "fieldConfig": {
-            "defaults": {
-              "custom": {},
-              "links": []
-            },
-            "overrides": []
-          },
-          "fill": 1,
-          "fillGradient": 5,
-          "gridPos": {
-            "h": 8,
-            "w": 8,
-            "x": 16,
-            "y": 3
-          },
-          "hiddenSeries": false,
-          "id": 175,
-          "legend": {
-            "alignAsTable": true,
-            "avg": true,
-            "current": true,
-            "hideEmpty": true,
-            "hideZero": true,
-            "max": true,
-            "min": false,
-            "rightSide": false,
-            "show": true,
-            "sideWidth": null,
-            "sort": null,
-            "sortDesc": null,
-            "total": false,
-            "values": true
-          },
-          "lines": true,
-          "linewidth": 2,
-          "links": [],
-          "maxPerRow": 6,
-          "nullPointMode": "null",
-          "options": {
-            "alertThreshold": true
-          },
-          "percentage": false,
-          "pluginVersion": "7.4.3",
-          "pointradius": 5,
-          "points": false,
-          "renderer": "flot",
-          "seriesOverrides": [],
-          "spaceLength": 10,
-          "stack": false,
-          "steppedLine": false,
-          "targets": [
-            {
-              "expr": "irate(diskio_io_time{name!~\"loop.*\"}[$__rate_interval])",
-              "format": "time_series",
-              "interval": "",
-              "intervalFactor": 1,
-              "legendFormat": "{{juju_unit}}_{{name}}_ IO time",
-              "refId": "C"
-            }
-          ],
-          "thresholds": [],
-          "timeFrom": null,
-          "timeRegions": [],
-          "timeShift": null,
-          "title": "Time Spent Doing I/Os",
-          "tooltip": {
-            "shared": true,
-            "sort": 2,
-            "value_type": "individual"
-          },
-          "type": "graph",
-          "xaxis": {
-            "buckets": null,
-            "mode": "time",
-            "name": null,
-            "show": true,
-            "values": []
-          },
-          "yaxes": [
-            {
-              "decimals": null,
-              "format": "s",
-              "label": "",
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            },
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": false
-            }
-          ],
-          "yaxis": {
-            "align": false,
-            "alignLevel": null
-          }
-        }
-      ],
-      "title": "Disk",
-      "type": "row"
-    },
-    {
-      "collapsed": true,
-      "datasource": null,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 9
-      },
-      "id": 185,
-      "panels": [
-        {
-          "aliasColors": {},
-          "bars": false,
-          "dashLength": 10,
-          "dashes": false,
-          "datasource": "prometheus - Juju generated source",
-          "decimals": 2,
-          "fieldConfig": {
-            "defaults": {
-              "custom": {},
-              "links": []
-            },
-            "overrides": []
-          },
-          "fill": 1,
-          "fillGradient": 0,
-          "gridPos": {
-            "h": 8,
-            "w": 24,
-            "x": 0,
-            "y": 12
-          },
-          "height": "300",
-          "hiddenSeries": false,
-          "id": 156,
-          "legend": {
-            "alignAsTable": true,
-            "avg": false,
-            "current": true,
-            "max": false,
-            "min": false,
-            "rightSide": false,
-            "show": true,
-            "sort": "current",
-            "sortDesc": true,
-            "total": false,
-            "values": true
-          },
-          "lines": true,
-          "linewidth": 2,
-          "links": [],
-          "nullPointMode": "null",
-          "options": {
-            "alertThreshold": true
-          },
-          "percentage": false,
-          "pluginVersion": "7.4.3",
-          "pointradius": 5,
-          "points": false,
-          "renderer": "flot",
-          "seriesOverrides": [
-            {
-              "$$hashKey": "object:2450",
-              "alias": "/.*_Total/",
-              "color": "#C4162A",
-              "fill": 0
-            }
-          ],
-          "spaceLength": 10,
-          "stack": false,
-          "steppedLine": false,
-          "targets": [
-            {
-              "expr": "mem_total",
-              "format": "time_series",
-              "hide": false,
-              "instant": false,
-              "interval": "",
-              "intervalFactor": 1,
-              "legendFormat": "{{juju_unit}}_Total",
-              "refId": "A",
-              "step": 4
-            },
-            {
-              "expr": "mem_used",
-              "format": "time_series",
-              "hide": false,
-              "interval": "",
-              "intervalFactor": 1,
-              "legendFormat": "{{juju_unit}}_Used",
-              "refId": "B",
-              "step": 4
-            },
-            {
-              "expr": "mem_available",
-              "format": "time_series",
-              "hide": false,
-              "interval": "",
-              "intervalFactor": 1,
-              "legendFormat": "{{juju_unit}}_Avaliable",
-              "refId": "F",
-              "step": 4
-            }
-          ],
-          "thresholds": [],
-          "timeFrom": null,
-          "timeRegions": [],
-          "timeShift": null,
-          "title": "Memory",
-          "tooltip": {
-            "shared": true,
-            "sort": 2,
-            "value_type": "individual"
-          },
-          "type": "graph",
-          "xaxis": {
-            "buckets": null,
-            "mode": "time",
-            "name": null,
-            "show": true,
-            "values": []
-          },
-          "yaxes": [
-            {
-              "$$hashKey": "object:2459",
-              "format": "bytes",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": "0",
-              "show": true
-            },
-            {
-              "$$hashKey": "object:2460",
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            }
-          ],
-          "yaxis": {
-            "align": false,
-            "alignLevel": null
-          }
-        }
-      ],
-      "title": "Memory",
-      "type": "row"
-    },
-    {
-      "collapsed": true,
-      "datasource": null,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 10
-      },
-      "id": 187,
-      "panels": [
-        {
-          "aliasColors": {},
-          "bars": false,
-          "dashLength": 10,
-          "dashes": false,
-          "datasource": "prometheus - Juju generated source",
-          "fieldConfig": {
-            "defaults": {
-              "custom": {},
-              "links": []
-            },
-            "overrides": []
-          },
-          "fill": 1,
-          "fillGradient": 3,
-          "gridPos": {
-            "h": 12,
-            "w": 12,
-            "x": 0,
-            "y": 13
-          },
-          "height": "300",
-          "hiddenSeries": false,
-          "id": 157,
-          "legend": {
-            "alignAsTable": true,
-            "avg": false,
-            "current": true,
-            "hideEmpty": true,
-            "hideZero": true,
-            "max": true,
-            "min": false,
-            "rightSide": false,
-            "show": true,
-            "sort": "current",
-            "sortDesc": true,
-            "total": false,
-            "values": true
-          },
-          "lines": true,
-          "linewidth": 2,
-          "links": [],
-          "nullPointMode": "null",
-          "options": {
-            "alertThreshold": true
-          },
-          "percentage": false,
-          "pluginVersion": "7.4.3",
-          "pointradius": 2,
-          "points": false,
-          "renderer": "flot",
-          "seriesOverrides": [
-            {
-              "$$hashKey": "object:2498",
-              "alias": "/.*_transmit$/",
-              "transform": "negative-Y"
-            }
-          ],
-          "spaceLength": 10,
-          "stack": false,
-          "steppedLine": false,
-          "targets": [
-            {
-              "expr": "irate(net_bytes_recv{interface!~'tap.*|veth.*|br.*|docker.*|virbr*|lo*'}[$__rate_interval])*8",
-              "format": "time_series",
-              "interval": "",
-              "intervalFactor": 1,
-              "legendFormat": "{{juju_unit}}_{{interface}}_receive",
-              "refId": "A",
-              "step": 4
-            },
-            {
-              "expr": "irate(net_bytes_sent{interface!~'tap.*|veth.*|br.*|docker.*|virbr*|lo*'}[$__rate_interval])*8",
-              "format": "time_series",
-              "interval": "",
-              "intervalFactor": 1,
-              "legendFormat": "{{instance}}_{{device}}_transmit",
-              "refId": "B",
-              "step": 4
-            }
-          ],
-          "thresholds": [],
-          "timeFrom": null,
-          "timeRegions": [],
-          "timeShift": null,
-          "title": "Network Traffic",
-          "tooltip": {
-            "shared": true,
-            "sort": 2,
-            "value_type": "individual"
-          },
-          "type": "graph",
-          "xaxis": {
-            "buckets": null,
-            "mode": "time",
-            "name": null,
-            "show": true,
-            "values": []
-          },
-          "yaxes": [
-            {
-              "$$hashKey": "object:2505",
-              "format": "bps",
-              "label": "transmit(-)/receive(+)",
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            },
-            {
-              "$$hashKey": "object:2506",
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": false
-            }
-          ],
-          "yaxis": {
-            "align": false,
-            "alignLevel": null
-          }
-        },
-        {
-          "aliasColors": {
-            "TCP": "#6ED0E0"
-          },
-          "bars": false,
-          "dashLength": 10,
-          "dashes": false,
-          "datasource": "prometheus - Juju generated source",
-          "description": "TCP_alloc - Allocated sockets\n\nCurrEstab - TCP connections for which the current state is either ESTABLISHED or CLOSE- WAIT\n\nTCP_tw - Sockets wating close\n\nUDP_inuse - Udp sockets currently in use\n\nSockets_used - Sockets currently in use",
-          "fieldConfig": {
-            "defaults": {
-              "custom": {},
-              "links": []
-            },
-            "overrides": []
-          },
-          "fill": 1,
-          "fillGradient": 0,
-          "gridPos": {
-            "h": 12,
-            "w": 12,
-            "x": 12,
-            "y": 13
-          },
-          "height": "300",
-          "hiddenSeries": false,
-          "id": 158,
-          "interval": "",
-          "legend": {
-            "alignAsTable": true,
-            "avg": true,
-            "current": true,
-            "hideEmpty": true,
-            "hideZero": true,
-            "max": true,
-            "min": false,
-            "rightSide": false,
-            "show": true,
-            "sort": "current",
-            "sortDesc": true,
-            "total": false,
-            "values": true
-          },
-          "lines": true,
-          "linewidth": 2,
-          "links": [],
-          "nullPointMode": "null",
-          "options": {
-            "alertThreshold": true
-          },
-          "percentage": false,
-          "pluginVersion": "7.4.3",
-          "pointradius": 5,
-          "points": false,
-          "renderer": "flot",
-          "seriesOverrides": [
-            {
-              "$$hashKey": "object:2576",
-              "alias": "/.*_Sockets_used/",
-              "color": "#C4162A",
-              "fill": 0
-            }
-          ],
-          "spaceLength": 10,
-          "stack": false,
-          "steppedLine": false,
-          "targets": [
-            {
-              "expr": "netstat_tcp_established",
-              "format": "time_series",
-              "hide": false,
-              "instant": false,
-              "interval": "",
-              "intervalFactor": 1,
-              "legendFormat": "{{juju_unit}}_CurrEstab",
-              "refId": "A",
-              "step": 20
-            },
-            {
-              "expr": "sockstat_TCP_tw",
-              "format": "time_series",
-              "interval": "",
-              "intervalFactor": 1,
-              "legendFormat": "{{juju_unit}}_TCP_tw",
-              "refId": "D"
-            },
-            {
-              "expr": "sockstat_sockets_used",
-              "interval": "",
-              "legendFormat": "{{juju_unit}}_Sockets_used",
-              "refId": "B"
-            },
-            {
-              "expr": "sockstat_UDP_inuse",
-              "interval": "",
-              "legendFormat": "{{juju_unit}}_UDP_inuse",
-              "refId": "C"
-            },
-            {
-              "expr": "sockstat_TCP_alloc",
-              "interval": "",
-              "legendFormat": "{{juju_unit}}_TCP_alloc",
-              "refId": "E"
-            }
-          ],
-          "thresholds": [],
-          "timeFrom": null,
-          "timeRegions": [],
-          "timeShift": null,
-          "title": "Network Sockstat",
-          "tooltip": {
-            "shared": true,
-            "sort": 2,
-            "value_type": "individual"
-          },
-          "type": "graph",
-          "xaxis": {
-            "buckets": null,
-            "mode": "time",
-            "name": null,
-            "show": true,
-            "values": []
-          },
-          "yaxes": [
-            {
-              "$$hashKey": "object:2585",
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            },
-            {
-              "$$hashKey": "object:2586",
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            }
-          ],
-          "yaxis": {
-            "align": false,
-            "alignLevel": null
-          }
-        }
-      ],
-      "title": "Network",
-      "type": "row"
-    }
-  ],
-  "refresh": false,
-  "schemaVersion": 27,
-  "style": "dark",
-  "tags": [],
-  "templating": {
-    "list": [
-      {
-        "allValue": null,
-        "current": {
-          "isNone": true,
-          "selected": false,
-          "text": "None",
-          "value": ""
-        },
-        "datasource": "prometheus - Juju generated source",
-        "definition": "label_values(node_uname_info, job)",
-        "description": null,
-        "error": null,
-        "hide": 0,
-        "includeAll": false,
-        "label": "JOB",
-        "multi": false,
-        "name": "job",
-        "options": [],
-        "query": {
-          "query": "label_values(node_uname_info, job)",
-          "refId": "prometheus - Juju generated source-job-Variable-Query"
-        },
-        "refresh": 1,
-        "regex": "",
-        "skipUrlSync": false,
-        "sort": 1,
-        "tagValuesQuery": "",
-        "tags": [],
-        "tagsQuery": "",
-        "type": "query",
-        "useTags": false
-      },
-      {
-        "allValue": null,
-        "current": {
-          "selected": false,
-          "text": "All",
-          "value": "$__all"
-        },
-        "datasource": "prometheus - Juju generated source",
-        "definition": "label_values(node_uname_info{job=~\"$job\"}, nodename)",
-        "description": null,
-        "error": null,
-        "hide": 0,
-        "includeAll": true,
-        "label": "Host",
-        "multi": true,
-        "name": "hostname",
-        "options": [],
-        "query": {
-          "query": "label_values(node_uname_info{job=~\"$job\"}, nodename)",
-          "refId": "prometheus - Juju generated source-hostname-Variable-Query"
-        },
-        "refresh": 1,
-        "regex": "",
-        "skipUrlSync": false,
-        "sort": 0,
-        "tagValuesQuery": "",
-        "tags": [],
-        "tagsQuery": "",
-        "type": "query",
-        "useTags": false
-      },
-      {
-        "allFormat": "glob",
-        "allValue": null,
-        "current": {
-          "selected": false,
-          "text": "All",
-          "value": "$__all"
-        },
-        "datasource": "prometheus - Juju generated source",
-        "definition": "label_values(node_uname_info{nodename=~\"$hostname\"},instance)",
-        "description": null,
-        "error": null,
-        "hide": 0,
-        "includeAll": true,
-        "label": "IP",
-        "multi": false,
-        "multiFormat": "regex values",
-        "name": "node",
-        "options": [],
-        "query": {
-          "query": "label_values(node_uname_info{nodename=~\"$hostname\"},instance)",
-          "refId": "prometheus - Juju generated source-node-Variable-Query"
-        },
-        "refresh": 2,
-        "regex": "",
-        "skipUrlSync": false,
-        "sort": 1,
-        "tagValuesQuery": "",
-        "tags": [],
-        "tagsQuery": "",
-        "type": "query",
-        "useTags": false
-      },
-      {
-        "allValue": null,
-        "current": {
-          "isNone": true,
-          "selected": false,
-          "text": "None",
-          "value": ""
-        },
-        "datasource": "prometheus - Juju generated source",
-        "definition": "",
-        "description": null,
-        "error": null,
-        "hide": 2,
-        "includeAll": false,
-        "label": "",
-        "multi": false,
-        "name": "maxmount",
-        "options": [],
-        "query": {
-          "query": "query_result(topk(1,sort_desc (max(node_filesystem_size_bytes{instance=~'$node',fstype=~\"ext4|xfs\"}) by (mountpoint))))",
-          "refId": "prometheus - Juju generated source-maxmount-Variable-Query"
-        },
-        "refresh": 2,
-        "regex": "/.*\\\"(.*)\\\".*/",
-        "skipUrlSync": false,
-        "sort": 0,
-        "tagValuesQuery": "",
-        "tags": [],
-        "tagsQuery": "",
-        "type": "query",
-        "useTags": false
-      },
-      {
-        "allFormat": "glob",
-        "allValue": null,
-        "current": {
-          "isNone": true,
-          "selected": false,
-          "text": "None",
-          "value": ""
-        },
-        "datasource": "prometheus - Juju generated source",
-        "definition": "",
-        "description": null,
-        "error": null,
-        "hide": 2,
-        "includeAll": false,
-        "label": null,
-        "multi": false,
-        "multiFormat": "regex values",
-        "name": "env",
-        "options": [],
-        "query": {
-          "query": "label_values(node_exporter_build_info,env)",
-          "refId": "prometheus - Juju generated source-env-Variable-Query"
-        },
-        "refresh": 2,
-        "regex": "",
-        "skipUrlSync": false,
-        "sort": 1,
-        "tagValuesQuery": "",
-        "tags": [],
-        "tagsQuery": "",
-        "type": "query",
-        "useTags": false
-      },
-      {
-        "allFormat": "glob",
-        "allValue": "",
-        "current": {
-          "isNone": true,
-          "selected": false,
-          "text": "None",
-          "value": ""
-        },
-        "datasource": "prometheus - Juju generated source",
-        "definition": "label_values(node_exporter_build_info{env=~'$env'},name)",
-        "description": null,
-        "error": null,
-        "hide": 2,
-        "includeAll": false,
-        "label": "名称",
-        "multi": true,
-        "multiFormat": "regex values",
-        "name": "name",
-        "options": [],
-        "query": {
-          "query": "label_values(node_exporter_build_info{env=~'$env'},name)",
-          "refId": "prometheus - Juju generated source-name-Variable-Query"
-        },
-        "refresh": 2,
-        "regex": "",
-        "skipUrlSync": false,
-        "sort": 1,
-        "tagValuesQuery": "/.*/",
-        "tags": [],
-        "tagsQuery": "",
-        "type": "query",
-        "useTags": false
-      }
-    ]
-  },
-  "time": {
-    "from": "now-2d",
-    "to": "now"
-  },
-  "timepicker": {
-    "now": true,
-    "refresh_intervals": [
-      "5s",
-      "10s",
-      "30s",
-      "1m",
-      "5m",
-      "15m",
-      "30m",
-      "1h",
-      "2h",
-      "1d"
-    ],
-    "time_options": [
-      "5m",
-      "15m",
-      "1h",
-      "6h",
-      "12h",
-      "24h",
-      "2d",
-      "7d",
-      "30d"
-    ]
-  },
-  "timezone": "browser",
-  "title": "Hosts",
-  "uid": "ha7fSE0Zz",
-  "version": 16
-}
\ No newline at end of file
diff --git a/installers/charm/grafana/templates/summary_dashboard.json b/installers/charm/grafana/templates/summary_dashboard.json
deleted file mode 100644 (file)
index bf709d6..0000000
+++ /dev/null
@@ -1,2112 +0,0 @@
-{
-  "annotations": {
-    "list": [
-      {
-        "builtIn": 1,
-        "datasource": "-- Grafana --",
-        "enable": true,
-        "hide": true,
-        "iconColor": "rgba(0, 211, 255, 1)",
-        "name": "Annotations & Alerts",
-        "type": "dashboard"
-      }
-    ]
-  },
-  "description": "OSM status summary",
-  "editable": true,
-  "gnetId": 6417,
-  "graphTooltip": 1,
-  "id": 5,
-  "iteration": 1615160504049,
-  "links": [
-    {
-      "asDropdown": true,
-      "icon": "external link",
-      "includeVars": true,
-      "keepTime": false,
-      "tags": [],
-      "title": "Dashboards",
-      "type": "dashboards"
-    }
-  ],
-  "panels": [
-    {
-      "collapsed": false,
-      "datasource": null,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 0
-      },
-      "id": 2,
-      "panels": [],
-      "title": "Cluster Health",
-      "type": "row"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": false,
-      "colorPrefix": false,
-      "colorValue": false,
-      "colors": [
-        "#299c46",
-        "rgba(237, 129, 40, 0.89)",
-        "#d44a3a"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "none",
-      "gauge": {
-        "maxValue": 100,
-        "minValue": 0,
-        "show": false,
-        "thresholdLabels": false,
-        "thresholdMarkers": true
-      },
-      "gridPos": {
-        "h": 2,
-        "w": 12,
-        "x": 0,
-        "y": 1
-      },
-      "id": 26,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "postfix": " Nodes",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "repeat": null,
-      "repeatDirection": "h",
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": false
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "sum(kube_node_info)",
-          "format": "time_series",
-          "instant": true,
-          "interval": "",
-          "intervalFactor": 1,
-          "legendFormat": "",
-          "refId": "B"
-        }
-      ],
-      "thresholds": "1",
-      "title": "",
-      "type": "singlestat",
-      "valueFontSize": "70%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "N/A",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": false,
-      "colorValue": false,
-      "colors": [
-        "#299c46",
-        "rgba(237, 129, 40, 0.89)",
-        "#d44a3a"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "none",
-      "gauge": {
-        "maxValue": 100,
-        "minValue": 0,
-        "show": false,
-        "thresholdLabels": false,
-        "thresholdMarkers": true
-      },
-      "gridPos": {
-        "h": 2,
-        "w": 12,
-        "x": 12,
-        "y": 1
-      },
-      "id": 30,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "postfix": " Pods Running",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(78, 203, 42, 0.28)",
-        "full": false,
-        "lineColor": "#629e51",
-        "show": true
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "sum(kube_pod_status_phase)",
-          "format": "time_series",
-          "instant": true,
-          "interval": "",
-          "intervalFactor": 1,
-          "legendFormat": "",
-          "refId": "A"
-        }
-      ],
-      "thresholds": "",
-      "title": "",
-      "type": "singlestat",
-      "valueFontSize": "70%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "N/A",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": true,
-      "colorPrefix": false,
-      "colorValue": false,
-      "colors": [
-        "#56A64B",
-        "rgba(237, 129, 40, 0.89)",
-        "#d44a3a"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "none",
-      "gauge": {
-        "maxValue": 100,
-        "minValue": 0,
-        "show": false,
-        "thresholdLabels": false,
-        "thresholdMarkers": true
-      },
-      "gridPos": {
-        "h": 2,
-        "w": 12,
-        "x": 0,
-        "y": 3
-      },
-      "id": 24,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "postfix": " Nodes Unavailable",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": false
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "sum(kube_node_info)-sum(kube_node_status_condition{condition=\"Ready\", status=\"true\"})",
-          "format": "time_series",
-          "instant": true,
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": "1,1",
-      "title": "",
-      "type": "singlestat",
-      "valueFontSize": "70%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "N/A",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": true,
-      "colorValue": false,
-      "colors": [
-        "#56A64B",
-        "rgba(237, 129, 40, 0.89)",
-        "#d44a3a"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "none",
-      "gauge": {
-        "maxValue": 100,
-        "minValue": 0,
-        "show": false,
-        "thresholdLabels": false,
-        "thresholdMarkers": true
-      },
-      "gridPos": {
-        "h": 2,
-        "w": 12,
-        "x": 12,
-        "y": 3
-      },
-      "id": 55,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "postfix": " Pods not Ready",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": false,
-        "ymax": null,
-        "ymin": null
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "sum(kube_pod_status_phase{phase!=\"Running\"})",
-          "instant": true,
-          "interval": "",
-          "legendFormat": "",
-          "refId": "A"
-        }
-      ],
-      "thresholds": "1",
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "",
-      "type": "singlestat",
-      "valueFontSize": "70%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "N/A",
-          "value": "null"
-        }
-      ],
-      "valueName": "avg"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": false,
-      "colorValue": false,
-      "colors": [
-        "#299c46",
-        "rgba(237, 129, 40, 0.89)",
-        "#d44a3a"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "percentunit",
-      "gauge": {
-        "maxValue": 1,
-        "minValue": 0,
-        "show": true,
-        "thresholdLabels": false,
-        "thresholdMarkers": true
-      },
-      "gridPos": {
-        "h": 4,
-        "w": 6,
-        "x": 0,
-        "y": 5
-      },
-      "id": 4,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": false
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "sum(kube_pod_info) / sum(kube_node_status_allocatable_pods)",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": "0.7,0.85",
-      "title": "Pod Usage",
-      "type": "singlestat",
-      "valueFontSize": "80%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "N/A",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": false,
-      "colorValue": false,
-      "colors": [
-        "#299c46",
-        "rgba(237, 129, 40, 0.89)",
-        "#d44a3a"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "percentunit",
-      "gauge": {
-        "maxValue": 1,
-        "minValue": 0,
-        "show": true,
-        "thresholdLabels": false,
-        "thresholdMarkers": true
-      },
-      "gridPos": {
-        "h": 4,
-        "w": 6,
-        "x": 6,
-        "y": 5
-      },
-      "id": 5,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": false
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "sum(kube_pod_container_resource_requests_cpu_cores) / sum(kube_node_status_allocatable_cpu_cores)",
-          "format": "time_series",
-          "instant": true,
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": "0.7,0.85",
-      "title": "CPU Usage",
-      "type": "singlestat",
-      "valueFontSize": "80%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "N/A",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": false,
-      "colorValue": false,
-      "colors": [
-        "#299c46",
-        "rgba(237, 129, 40, 0.89)",
-        "#d44a3a"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "percentunit",
-      "gauge": {
-        "maxValue": 1,
-        "minValue": 0,
-        "show": true,
-        "thresholdLabels": false,
-        "thresholdMarkers": true
-      },
-      "gridPos": {
-        "h": 4,
-        "w": 6,
-        "x": 12,
-        "y": 5
-      },
-      "id": 6,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": false
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "sum(kube_pod_container_resource_requests_memory_bytes) / sum(kube_node_status_allocatable_memory_bytes)",
-          "format": "time_series",
-          "instant": true,
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": "0.7,0.85",
-      "title": "Memory Usage",
-      "type": "singlestat",
-      "valueFontSize": "80%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "N/A",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": false,
-      "colorValue": false,
-      "colors": [
-        "#73BF69",
-        "rgba(237, 129, 40, 0.89)",
-        "#d44a3a"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "percentunit",
-      "gauge": {
-        "maxValue": 1,
-        "minValue": 0,
-        "show": true,
-        "thresholdLabels": false,
-        "thresholdMarkers": true
-      },
-      "gridPos": {
-        "h": 4,
-        "w": 6,
-        "x": 18,
-        "y": 5
-      },
-      "id": 7,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": false
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "(sum (node_filesystem_size_bytes) - sum (node_filesystem_free_bytes)) / sum (node_filesystem_size_bytes)",
-          "format": "time_series",
-          "instant": true,
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": "0.7,0.85",
-      "title": "Disk Usage",
-      "type": "singlestat",
-      "valueFontSize": "80%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "N/A",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "collapsed": false,
-      "datasource": null,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 9
-      },
-      "id": 61,
-      "panels": [],
-      "title": "OSM",
-      "type": "row"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": false,
-      "colorValue": false,
-      "colors": [
-        "#d44a3a",
-        "rgba(237, 129, 40, 0.89)",
-        "#299c46"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "none",
-      "gauge": {
-        "maxValue": 1,
-        "minValue": 0,
-        "show": true,
-        "thresholdLabels": false,
-        "thresholdMarkers": false
-      },
-      "gridPos": {
-        "h": 4,
-        "w": 3,
-        "x": 0,
-        "y": 10
-      },
-      "id": 71,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": false,
-        "ymax": null,
-        "ymin": null
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "kube_statefulset_status_replicas_ready{namespace=\"osm\", statefulset=\"prometheus-k8s\"}",
-          "format": "time_series",
-          "interval": "",
-          "legendFormat": "",
-          "refId": "A"
-        }
-      ],
-      "thresholds": "0,1",
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Prometheus",
-      "type": "singlestat",
-      "valueFontSize": "100%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "0",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": false,
-      "colorValue": false,
-      "colors": [
-        "#d44a3a",
-        "rgba(237, 129, 40, 0.89)",
-        "#299c46"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "decimals": null,
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "none",
-      "gauge": {
-        "maxValue": 1,
-        "minValue": 0,
-        "show": true,
-        "thresholdLabels": false,
-        "thresholdMarkers": false
-      },
-      "gridPos": {
-        "h": 4,
-        "w": 3,
-        "x": 4,
-        "y": 10
-      },
-      "id": 74,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": false,
-        "ymax": null,
-        "ymin": null
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "kube_statefulset_status_replicas_ready{namespace=\"osm\", statefulset=\"mongodb-k8s\"}",
-          "format": "time_series",
-          "refId": "A"
-        }
-      ],
-      "thresholds": "0,1",
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "mongo",
-      "type": "singlestat",
-      "valueFontSize": "100%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "0",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": false,
-      "colorValue": false,
-      "colors": [
-        "#d44a3a",
-        "rgba(237, 129, 40, 0.89)",
-        "#299c46"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "none",
-      "gauge": {
-        "maxValue": 1,
-        "minValue": 0,
-        "show": true,
-        "thresholdLabels": false,
-        "thresholdMarkers": false
-      },
-      "gridPos": {
-        "h": 4,
-        "w": 3,
-        "x": 8,
-        "y": 10
-      },
-      "id": 72,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": false,
-        "ymax": null,
-        "ymin": null
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "kube_statefulset_status_replicas_ready{namespace=\"osm\", statefulset=\"mariadb-k8s\"}",
-          "format": "time_series",
-          "interval": "",
-          "legendFormat": "",
-          "refId": "A"
-        }
-      ],
-      "thresholds": "0,1",
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "mysql ",
-      "type": "singlestat",
-      "valueFontSize": "100%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "0",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": false,
-      "colorValue": false,
-      "colors": [
-        "#d44a3a",
-        "rgba(237, 129, 40, 0.89)",
-        "#299c46"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "none",
-      "gauge": {
-        "maxValue": 1,
-        "minValue": 0,
-        "show": true,
-        "thresholdLabels": false,
-        "thresholdMarkers": false
-      },
-      "gridPos": {
-        "h": 4,
-        "w": 3,
-        "x": 12,
-        "y": 10
-      },
-      "id": 77,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "pluginVersion": "6.3.5",
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": false,
-        "ymax": null,
-        "ymin": null
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "kube_statefulset_status_replicas_ready{namespace=\"osm\", statefulset=\"ro-k8s\"}",
-          "format": "time_series",
-          "instant": true,
-          "interval": "",
-          "legendFormat": "",
-          "refId": "A"
-        }
-      ],
-      "thresholds": "0,1",
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "ro",
-      "type": "singlestat",
-      "valueFontSize": "100%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "0",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": false,
-      "colorValue": false,
-      "colors": [
-        "#d44a3a",
-        "rgba(237, 129, 40, 0.89)",
-        "#299c46"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "decimals": null,
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "none",
-      "gauge": {
-        "maxValue": 1,
-        "minValue": 0,
-        "show": true,
-        "thresholdLabels": false,
-        "thresholdMarkers": false
-      },
-      "gridPos": {
-        "h": 4,
-        "w": 3,
-        "x": 16,
-        "y": 10
-      },
-      "id": 73,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": false,
-        "ymax": null,
-        "ymin": null
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "kube_statefulset_status_replicas_ready{namespace=\"osm\", statefulset=\"zookeeper-k8s\"}",
-          "format": "time_series",
-          "interval": "",
-          "legendFormat": "",
-          "refId": "A"
-        }
-      ],
-      "thresholds": "0,1",
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "zookeeper",
-      "type": "singlestat",
-      "valueFontSize": "100%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "0",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": false,
-      "colorValue": false,
-      "colors": [
-        "#d44a3a",
-        "rgba(237, 129, 40, 0.89)",
-        "#299c46"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "decimals": null,
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "none",
-      "gauge": {
-        "maxValue": 1,
-        "minValue": 0,
-        "show": true,
-        "thresholdLabels": false,
-        "thresholdMarkers": false
-      },
-      "gridPos": {
-        "h": 4,
-        "w": 3,
-        "x": 20,
-        "y": 10
-      },
-      "id": 78,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": false,
-        "ymax": null,
-        "ymin": null
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "kube_statefulset_status_replicas_ready{namespace=\"osm\", statefulset=\"kafka-k8s\"}",
-          "format": "time_series",
-          "interval": "",
-          "legendFormat": "",
-          "refId": "A"
-        }
-      ],
-      "thresholds": "0,1",
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "kafka",
-      "type": "singlestat",
-      "valueFontSize": "100%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "0",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": false,
-      "colorValue": false,
-      "colors": [
-        "#d44a3a",
-        "rgba(237, 129, 40, 0.89)",
-        "#299c46"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "none",
-      "gauge": {
-        "maxValue": 1,
-        "minValue": 0,
-        "show": true,
-        "thresholdLabels": false,
-        "thresholdMarkers": false
-      },
-      "gridPos": {
-        "h": 4,
-        "w": 3,
-        "x": 0,
-        "y": 14
-      },
-      "id": 76,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "pluginVersion": "6.3.5",
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": false,
-        "ymax": null,
-        "ymin": null
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "kube_statefulset_status_replicas_ready{namespace=\"osm\", statefulset=\"lcm-k8s\"}",
-          "format": "time_series",
-          "instant": true,
-          "interval": "",
-          "legendFormat": "",
-          "refId": "A"
-        }
-      ],
-      "thresholds": "0,1",
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "lcm",
-      "type": "singlestat",
-      "valueFontSize": "100%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "0",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": false,
-      "colorValue": false,
-      "colors": [
-        "#d44a3a",
-        "rgba(237, 129, 40, 0.89)",
-        "#299c46"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "description": "",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "none",
-      "gauge": {
-        "maxValue": 1,
-        "minValue": 0,
-        "show": true,
-        "thresholdLabels": false,
-        "thresholdMarkers": false
-      },
-      "gridPos": {
-        "h": 4,
-        "w": 3,
-        "x": 8,
-        "y": 14
-      },
-      "id": 75,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "pluginVersion": "6.3.5",
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": false,
-        "ymax": null,
-        "ymin": null
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "kube_statefulset_status_replicas_ready{namespace=\"osm\", statefulset=\"nbi-k8s\"}",
-          "format": "time_series",
-          "instant": true,
-          "interval": "",
-          "legendFormat": "",
-          "refId": "A"
-        }
-      ],
-      "thresholds": "0,1",
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "nbi",
-      "type": "singlestat",
-      "valueFontSize": "100%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "0",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": false,
-      "colorValue": false,
-      "colors": [
-        "#d44a3a",
-        "rgba(237, 129, 40, 0.89)",
-        "#299c46"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "none",
-      "gauge": {
-        "maxValue": 1,
-        "minValue": 0,
-        "show": true,
-        "thresholdLabels": false,
-        "thresholdMarkers": false
-      },
-      "gridPos": {
-        "h": 4,
-        "w": 3,
-        "x": 12,
-        "y": 14
-      },
-      "id": 67,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "pluginVersion": "6.3.5",
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": false,
-        "ymax": null,
-        "ymin": null
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "kube_statefulset_status_replicas_ready{namespace=\"osm\", statefulset=\"pol-k8s\"}",
-          "format": "time_series",
-          "instant": true,
-          "interval": "",
-          "legendFormat": "",
-          "refId": "A"
-        }
-      ],
-      "thresholds": "0,1",
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "pol",
-      "type": "singlestat",
-      "valueFontSize": "100%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "0",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": false,
-      "colorValue": false,
-      "colors": [
-        "#d44a3a",
-        "rgba(237, 129, 40, 0.89)",
-        "#299c46"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "none",
-      "gauge": {
-        "maxValue": 1,
-        "minValue": 0,
-        "show": true,
-        "thresholdLabels": false,
-        "thresholdMarkers": false
-      },
-      "gridPos": {
-        "h": 4,
-        "w": 3,
-        "x": 16,
-        "y": 14
-      },
-      "id": 69,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "pluginVersion": "6.3.5",
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": false,
-        "ymax": null,
-        "ymin": null
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "kube_statefulset_status_replicas_ready{namespace=\"osm\", statefulset=\"mon-k8s\"}",
-          "instant": true,
-          "interval": "",
-          "legendFormat": "",
-          "refId": "A"
-        }
-      ],
-      "thresholds": "0,1",
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "mon",
-      "type": "singlestat",
-      "valueFontSize": "100%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "0",
-          "value": "null"
-        }
-      ],
-      "valueName": "avg"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": false,
-      "colorValue": false,
-      "colors": [
-        "#d44a3a",
-        "rgba(237, 129, 40, 0.89)",
-        "#299c46"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "none",
-      "gauge": {
-        "maxValue": 1,
-        "minValue": 0,
-        "show": true,
-        "thresholdLabels": false,
-        "thresholdMarkers": false
-      },
-      "gridPos": {
-        "h": 4,
-        "w": 3,
-        "x": 20,
-        "y": 14
-      },
-      "id": 81,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "pluginVersion": "6.3.5",
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": false,
-        "ymax": null,
-        "ymin": null
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "kube_deployment_status_replicas_available{deployment=\"keystone\"}",
-          "format": "time_series",
-          "instant": true,
-          "legendFormat": "",
-          "refId": "A"
-        }
-      ],
-      "thresholds": "0,1",
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "keystone",
-      "type": "singlestat",
-      "valueFontSize": "100%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "0",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {},
-          "links": []
-        },
-        "overrides": []
-      },
-      "fill": 6,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 9,
-        "w": 23,
-        "x": 0,
-        "y": 18
-      },
-      "hiddenSeries": false,
-      "id": 84,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "options": {
-        "alertThreshold": true
-      },
-      "percentage": false,
-      "pluginVersion": "7.4.3",
-      "pointradius": 2,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": true,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "sum(rate(container_cpu_usage_seconds_total{namespace=\"osm\", pod!~\".*operator.*\"}[$__rate_interval])) by (pod)",
-          "instant": false,
-          "interval": "",
-          "intervalFactor": 4,
-          "legendFormat": "{{pod}}",
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Pod CPU Usage",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "$$hashKey": "object:3755",
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "$$hashKey": "object:3756",
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {},
-          "links": []
-        },
-        "overrides": []
-      },
-      "fill": 6,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 9,
-        "w": 23,
-        "x": 0,
-        "y": 27
-      },
-      "hiddenSeries": false,
-      "id": 85,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "options": {
-        "alertThreshold": true
-      },
-      "percentage": false,
-      "pluginVersion": "7.4.3",
-      "pointradius": 2,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": true,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "sum(rate(container_memory_working_set_bytes{namespace=\"osm\", pod!~\".*operator.*\"}[$__rate_interval])) by (pod)",
-          "interval": "",
-          "intervalFactor": 4,
-          "legendFormat": "{{pod}}",
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Pod Memory Usage",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "$$hashKey": "object:3786",
-          "format": "decbytes",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "$$hashKey": "object:3787",
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "cacheTimeout": null,
-      "datasource": null,
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "gridPos": {
-        "h": 4,
-        "w": 3,
-        "x": 21,
-        "y": 36
-      },
-      "id": 82,
-      "links": [],
-      "options": {
-        "content": "\n\n\n",
-        "mode": "markdown"
-      },
-      "pluginVersion": "7.4.3",
-      "targets": [
-        {
-          "expr": "",
-          "instant": true,
-          "refId": "A"
-        }
-      ],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "",
-      "transparent": true,
-      "type": "text"
-    },
-    {
-      "cacheTimeout": null,
-      "datasource": null,
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "gridPos": {
-        "h": 4,
-        "w": 3,
-        "x": 19,
-        "y": 40
-      },
-      "id": 80,
-      "links": [],
-      "options": {
-        "content": "<h2 style=\"text-align: center;\"></p>\n\n\n",
-        "mode": "html"
-      },
-      "pluginVersion": "7.4.3",
-      "targets": [
-        {
-          "expr": "",
-          "instant": true,
-          "refId": "A"
-        }
-      ],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "",
-      "transparent": true,
-      "type": "text"
-    }
-  ],
-  "refresh": "30s",
-  "schemaVersion": 27,
-  "style": "dark",
-  "tags": [],
-  "templating": {
-    "list": [
-      {
-        "current": {
-          "selected": false,
-          "text": "No data sources found",
-          "value": ""
-        },
-        "description": null,
-        "error": null,
-        "hide": 2,
-        "includeAll": false,
-        "label": "",
-        "multi": false,
-        "name": "datasource",
-        "options": [],
-        "query": "prometheus",
-        "refresh": 1,
-        "regex": "/$ds/",
-        "skipUrlSync": false,
-        "type": "datasource"
-      }
-    ]
-  },
-  "time": {
-    "from": "now-15m",
-    "to": "now"
-  },
-  "timepicker": {
-    "refresh_intervals": [
-      "5s",
-      "10s",
-      "30s",
-      "1m",
-      "5m",
-      "15m",
-      "30m",
-      "1h",
-      "2h",
-      "1d"
-    ],
-    "time_options": [
-      "5m",
-      "15m",
-      "1h",
-      "6h",
-      "12h",
-      "24h",
-      "2d",
-      "7d",
-      "30d"
-    ]
-  },
-  "timezone": "browser",
-  "title": "OSM Status Summary",
-  "uid": "4XuPd2Ii1",
-  "version": 12
-}
\ No newline at end of file
diff --git a/installers/charm/grafana/tests/__init__.py b/installers/charm/grafana/tests/__init__.py
deleted file mode 100644 (file)
index 446d5ce..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2020 Canonical Ltd.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-#         http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-"""Init mocking for unit tests."""
-
-import sys
-
-
-import mock
-
-
-class OCIImageResourceErrorMock(Exception):
-    pass
-
-
-sys.path.append("src")
-
-oci_image = mock.MagicMock()
-oci_image.OCIImageResourceError = OCIImageResourceErrorMock
-sys.modules["oci_image"] = oci_image
-sys.modules["oci_image"].OCIImageResource().fetch.return_value = {}
diff --git a/installers/charm/grafana/tests/test_charm.py b/installers/charm/grafana/tests/test_charm.py
deleted file mode 100644 (file)
index 3bfd69c..0000000
+++ /dev/null
@@ -1,703 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2020 Canonical Ltd.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-#         http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-import sys
-from typing import NoReturn
-import unittest
-from unittest.mock import patch
-
-from charm import GrafanaCharm
-from ops.model import ActiveStatus, BlockedStatus
-from ops.testing import Harness
-
-
-class TestCharm(unittest.TestCase):
-    """Prometheus Charm unit tests."""
-
-    def setUp(self) -> NoReturn:
-        """Test setup"""
-        self.image_info = sys.modules["oci_image"].OCIImageResource().fetch()
-        self.harness = Harness(GrafanaCharm)
-        self.harness.set_leader(is_leader=True)
-        self.harness.begin()
-        self.config = {
-            "max_file_size": 0,
-            "ingress_whitelist_source_range": "",
-            "tls_secret_name": "",
-            "site_url": "https://grafana.192.168.100.100.nip.io",
-            "cluster_issuer": "vault-issuer",
-            "osm_dashboards": True,
-        }
-        self.harness.update_config(self.config)
-
-    def test_config_changed(
-        self,
-    ) -> NoReturn:
-        """Test ingress resources without HTTP."""
-
-        self.harness.charm.on.config_changed.emit()
-
-        # Assertions
-        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
-        self.assertTrue("prometheus" in self.harness.charm.unit.status.message)
-
-    def test_config_changed_non_leader(
-        self,
-    ) -> NoReturn:
-        """Test ingress resources without HTTP."""
-        self.harness.set_leader(is_leader=False)
-        self.harness.charm.on.config_changed.emit()
-
-        # Assertions
-        self.assertIsInstance(self.harness.charm.unit.status, ActiveStatus)
-
-    @patch("opslib.osm.interfaces.grafana.GrafanaCluster.set_initial_password")
-    def test_with_db_relation_and_prometheus(self, _) -> NoReturn:
-        self.initialize_prometheus_relation()
-        self.initialize_mysql_relation()
-        self.assertIsInstance(self.harness.charm.unit.status, ActiveStatus)
-
-    @patch("opslib.osm.interfaces.grafana.GrafanaCluster.set_initial_password")
-    def test_with_db_config_and_prometheus(self, _) -> NoReturn:
-        self.initialize_prometheus_relation()
-        self.initialize_mysql_config()
-        self.assertIsInstance(self.harness.charm.unit.status, ActiveStatus)
-
-    def test_with_prometheus(
-        self,
-    ) -> NoReturn:
-        """Test to see if prometheus relation is updated."""
-        self.initialize_prometheus_relation()
-        # Verifying status
-        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
-
-    def test_with_db_config(self) -> NoReturn:
-        "Test with mysql config"
-        self.initialize_mysql_config()
-        # Verifying status
-        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
-
-    @patch("opslib.osm.interfaces.grafana.GrafanaCluster.set_initial_password")
-    def test_with_db_relations(self, _) -> NoReturn:
-        "Test with relations"
-        self.initialize_mysql_relation()
-        # Verifying status
-        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
-
-    def test_exception_db_relation_and_config(
-        self,
-    ) -> NoReturn:
-        "Test with relations and config. Must throw exception"
-        self.initialize_mysql_config()
-        self.initialize_mysql_relation()
-        # Verifying status
-        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
-
-    def initialize_prometheus_relation(self):
-        relation_id = self.harness.add_relation("prometheus", "prometheus")
-        self.harness.add_relation_unit(relation_id, "prometheus/0")
-        self.harness.update_relation_data(
-            relation_id,
-            "prometheus",
-            {"hostname": "prometheus", "port": 9090},
-        )
-
-    def initialize_mysql_config(self):
-        self.harness.update_config(
-            {"mysql_uri": "mysql://grafana:$grafanapw$@host:3606/db"}
-        )
-
-    def initialize_mysql_relation(self):
-        relation_id = self.harness.add_relation("db", "mysql")
-        self.harness.add_relation_unit(relation_id, "mysql/0")
-        self.harness.update_relation_data(
-            relation_id,
-            "mysql/0",
-            {
-                "host": "mysql",
-                "port": 3306,
-                "user": "mano",
-                "password": "manopw",
-                "root_password": "rootmanopw",
-            },
-        )
-
-
-if __name__ == "__main__":
-    unittest.main()
-
-# class TestCharm(unittest.TestCase):
-#     """Grafana Charm unit tests."""
-
-#     def setUp(self) -> NoReturn:
-#         """Test setup"""
-#         self.harness = Harness(GrafanaCharm)
-#         self.harness.set_leader(is_leader=True)
-#         self.harness.begin()
-
-#     def test_on_start_without_relations(self) -> NoReturn:
-#         """Test installation without any relation."""
-#         self.harness.charm.on.start.emit()
-
-#         # Verifying status
-#         self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
-
-#         # Verifying status message
-#         self.assertGreater(len(self.harness.charm.unit.status.message), 0)
-#         self.assertTrue(
-#             self.harness.charm.unit.status.message.startswith("Waiting for ")
-#         )
-#         self.assertIn("prometheus", self.harness.charm.unit.status.message)
-#         self.assertTrue(self.harness.charm.unit.status.message.endswith(" relation"))
-
-#     def test_on_start_with_relations_without_http(self) -> NoReturn:
-#         """Test deployment."""
-#         expected_result = {
-#             "version": 3,
-#             "containers": [
-#                 {
-#                     "name": "grafana",
-#                     "imageDetails": self.harness.charm.image.fetch(),
-#                     "imagePullPolicy": "Always",
-#                     "ports": [
-#                         {
-#                             "name": "grafana",
-#                             "containerPort": 3000,
-#                             "protocol": "TCP",
-#                         }
-#                     ],
-#                     "envConfig": {},
-#                     "volumeConfig": [
-#                         {
-#                             "name": "dashboards",
-#                             "mountPath": "/etc/grafana/provisioning/dashboards/",
-#                             "files": [
-#                                 {
-#                                     "path": "dashboard-osm.yml",
-#                                     "content": (
-#                                         "apiVersion: 1\n"
-#                                         "providers:\n"
-#                                         "  - name: 'osm'\n"
-#                                         "    orgId: 1\n"
-#                                         "    folder: ''\n"
-#                                         "    type: file\n"
-#                                         "    options:\n"
-#                                         "      path: /etc/grafana/provisioning/dashboards/\n"
-#                                     ),
-#                                 },
-#                             ],
-#                         },
-#                         {
-#                             "name": "datasources",
-#                             "mountPath": "/etc/grafana/provisioning/datasources/",
-#                             "files": [
-#                                 {
-#                                     "path": "datasource-prometheus.yml",
-#                                     "content": (
-#                                         "datasources:\n"
-#                                         "  - access: proxy\n"
-#                                         "    editable: true\n"
-#                                         "    is_default: true\n"
-#                                         "    name: osm_prometheus\n"
-#                                         "    orgId: 1\n"
-#                                         "    type: prometheus\n"
-#                                         "    version: 1\n"
-#                                         "    url: http://prometheus:9090\n"
-#                                     ),
-#                                 },
-#                             ],
-#                         },
-#                     ],
-#                     "kubernetes": {
-#                         "readinessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": 3000,
-#                             },
-#                             "initialDelaySeconds": 10,
-#                             "periodSeconds": 10,
-#                             "timeoutSeconds": 5,
-#                             "successThreshold": 1,
-#                             "failureThreshold": 3,
-#                         },
-#                         "livenessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": 3000,
-#                             },
-#                             "initialDelaySeconds": 60,
-#                             "timeoutSeconds": 30,
-#                             "failureThreshold": 10,
-#                         },
-#                     },
-#                 },
-#             ],
-#             "kubernetesResources": {"ingressResources": []},
-#         }
-
-#         self.harness.charm.on.start.emit()
-
-#         # Initializing the prometheus relation
-#         relation_id = self.harness.add_relation("prometheus", "prometheus")
-#         self.harness.add_relation_unit(relation_id, "prometheus/0")
-#         self.harness.update_relation_data(
-#             relation_id,
-#             "prometheus",
-#             {
-#                 "hostname": "prometheus",
-#                 "port": "9090",
-#             },
-#         )
-
-#         # Verifying status
-#         self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
-
-#         pod_spec, _ = self.harness.get_pod_spec()
-
-#         self.assertDictEqual(expected_result, pod_spec)
-
-#     def test_ingress_resources_with_http(self) -> NoReturn:
-#         """Test ingress resources with HTTP."""
-#         expected_result = {
-#             "version": 3,
-#             "containers": [
-#                 {
-#                     "name": "grafana",
-#                     "imageDetails": self.harness.charm.image.fetch(),
-#                     "imagePullPolicy": "Always",
-#                     "ports": [
-#                         {
-#                             "name": "grafana",
-#                             "containerPort": 3000,
-#                             "protocol": "TCP",
-#                         }
-#                     ],
-#                     "envConfig": {},
-#                     "volumeConfig": [
-#                         {
-#                             "name": "dashboards",
-#                             "mountPath": "/etc/grafana/provisioning/dashboards/",
-#                             "files": [
-#                                 {
-#                                     "path": "dashboard-osm.yml",
-#                                     "content": (
-#                                         "apiVersion: 1\n"
-#                                         "providers:\n"
-#                                         "  - name: 'osm'\n"
-#                                         "    orgId: 1\n"
-#                                         "    folder: ''\n"
-#                                         "    type: file\n"
-#                                         "    options:\n"
-#                                         "      path: /etc/grafana/provisioning/dashboards/\n"
-#                                     ),
-#                                 },
-#                             ],
-#                         },
-#                         {
-#                             "name": "datasources",
-#                             "mountPath": "/etc/grafana/provisioning/datasources/",
-#                             "files": [
-#                                 {
-#                                     "path": "datasource-prometheus.yml",
-#                                     "content": (
-#                                         "datasources:\n"
-#                                         "  - access: proxy\n"
-#                                         "    editable: true\n"
-#                                         "    is_default: true\n"
-#                                         "    name: osm_prometheus\n"
-#                                         "    orgId: 1\n"
-#                                         "    type: prometheus\n"
-#                                         "    version: 1\n"
-#                                         "    url: http://prometheus:9090\n"
-#                                     ),
-#                                 },
-#                             ],
-#                         },
-#                     ],
-#                     "kubernetes": {
-#                         "readinessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": 3000,
-#                             },
-#                             "initialDelaySeconds": 10,
-#                             "periodSeconds": 10,
-#                             "timeoutSeconds": 5,
-#                             "successThreshold": 1,
-#                             "failureThreshold": 3,
-#                         },
-#                         "livenessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": 3000,
-#                             },
-#                             "initialDelaySeconds": 60,
-#                             "timeoutSeconds": 30,
-#                             "failureThreshold": 10,
-#                         },
-#                     },
-#                 },
-#             ],
-#             "kubernetesResources": {
-#                 "ingressResources": [
-#                     {
-#                         "name": "grafana-ingress",
-#                         "annotations": {
-#                             "nginx.ingress.kubernetes.io/proxy-body-size": "0",
-#                             "nginx.ingress.kubernetes.io/ssl-redirect": "false",
-#                         },
-#                         "spec": {
-#                             "rules": [
-#                                 {
-#                                     "host": "grafana",
-#                                     "http": {
-#                                         "paths": [
-#                                             {
-#                                                 "path": "/",
-#                                                 "backend": {
-#                                                     "serviceName": "grafana",
-#                                                     "servicePort": 3000,
-#                                                 },
-#                                             }
-#                                         ]
-#                                     },
-#                                 }
-#                             ]
-#                         },
-#                     }
-#                 ],
-#             },
-#         }
-
-#         self.harness.charm.on.start.emit()
-
-#         # Initializing the prometheus relation
-#         relation_id = self.harness.add_relation("prometheus", "prometheus")
-#         self.harness.add_relation_unit(relation_id, "prometheus/0")
-#         self.harness.update_relation_data(
-#             relation_id,
-#             "prometheus",
-#             {
-#                 "hostname": "prometheus",
-#                 "port": "9090",
-#             },
-#         )
-
-#         self.harness.update_config({"site_url": "http://grafana"})
-
-#         pod_spec, _ = self.harness.get_pod_spec()
-
-#         self.assertDictEqual(expected_result, pod_spec)
-
-#     def test_ingress_resources_with_https(self) -> NoReturn:
-#         """Test ingress resources with HTTPS."""
-#         expected_result = {
-#             "version": 3,
-#             "containers": [
-#                 {
-#                     "name": "grafana",
-#                     "imageDetails": self.harness.charm.image.fetch(),
-#                     "imagePullPolicy": "Always",
-#                     "ports": [
-#                         {
-#                             "name": "grafana",
-#                             "containerPort": 3000,
-#                             "protocol": "TCP",
-#                         }
-#                     ],
-#                     "envConfig": {},
-#                     "volumeConfig": [
-#                         {
-#                             "name": "dashboards",
-#                             "mountPath": "/etc/grafana/provisioning/dashboards/",
-#                             "files": [
-#                                 {
-#                                     "path": "dashboard-osm.yml",
-#                                     "content": (
-#                                         "apiVersion: 1\n"
-#                                         "providers:\n"
-#                                         "  - name: 'osm'\n"
-#                                         "    orgId: 1\n"
-#                                         "    folder: ''\n"
-#                                         "    type: file\n"
-#                                         "    options:\n"
-#                                         "      path: /etc/grafana/provisioning/dashboards/\n"
-#                                     ),
-#                                 },
-#                             ],
-#                         },
-#                         {
-#                             "name": "datasources",
-#                             "mountPath": "/etc/grafana/provisioning/datasources/",
-#                             "files": [
-#                                 {
-#                                     "path": "datasource-prometheus.yml",
-#                                     "content": (
-#                                         "datasources:\n"
-#                                         "  - access: proxy\n"
-#                                         "    editable: true\n"
-#                                         "    is_default: true\n"
-#                                         "    name: osm_prometheus\n"
-#                                         "    orgId: 1\n"
-#                                         "    type: prometheus\n"
-#                                         "    version: 1\n"
-#                                         "    url: http://prometheus:9090\n"
-#                                     ),
-#                                 },
-#                             ],
-#                         },
-#                     ],
-#                     "kubernetes": {
-#                         "readinessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": 3000,
-#                             },
-#                             "initialDelaySeconds": 10,
-#                             "periodSeconds": 10,
-#                             "timeoutSeconds": 5,
-#                             "successThreshold": 1,
-#                             "failureThreshold": 3,
-#                         },
-#                         "livenessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": 3000,
-#                             },
-#                             "initialDelaySeconds": 60,
-#                             "timeoutSeconds": 30,
-#                             "failureThreshold": 10,
-#                         },
-#                     },
-#                 },
-#             ],
-#             "kubernetesResources": {
-#                 "ingressResources": [
-#                     {
-#                         "name": "grafana-ingress",
-#                         "annotations": {
-#                             "nginx.ingress.kubernetes.io/proxy-body-size": "0",
-#                         },
-#                         "spec": {
-#                             "rules": [
-#                                 {
-#                                     "host": "grafana",
-#                                     "http": {
-#                                         "paths": [
-#                                             {
-#                                                 "path": "/",
-#                                                 "backend": {
-#                                                     "serviceName": "grafana",
-#                                                     "servicePort": 3000,
-#                                                 },
-#                                             }
-#                                         ]
-#                                     },
-#                                 }
-#                             ],
-#                             "tls": [{"hosts": ["grafana"], "secretName": "grafana"}],
-#                         },
-#                     }
-#                 ],
-#             },
-#         }
-
-#         self.harness.charm.on.start.emit()
-
-#         # Initializing the prometheus relation
-#         relation_id = self.harness.add_relation("prometheus", "prometheus")
-#         self.harness.add_relation_unit(relation_id, "prometheus/0")
-#         self.harness.update_relation_data(
-#             relation_id,
-#             "prometheus",
-#             {
-#                 "hostname": "prometheus",
-#                 "port": "9090",
-#             },
-#         )
-
-#         self.harness.update_config(
-#             {"site_url": "https://grafana", "tls_secret_name": "grafana"}
-#         )
-
-#         pod_spec, _ = self.harness.get_pod_spec()
-
-#         self.assertDictEqual(expected_result, pod_spec)
-
-#     def test_ingress_resources_with_https_and_ingress_whitelist(self) -> NoReturn:
-#         """Test ingress resources with HTTPS and ingress whitelist."""
-#         expected_result = {
-#             "version": 3,
-#             "containers": [
-#                 {
-#                     "name": "grafana",
-#                     "imageDetails": self.harness.charm.image.fetch(),
-#                     "imagePullPolicy": "Always",
-#                     "ports": [
-#                         {
-#                             "name": "grafana",
-#                             "containerPort": 3000,
-#                             "protocol": "TCP",
-#                         }
-#                     ],
-#                     "envConfig": {},
-#                     "volumeConfig": [
-#                         {
-#                             "name": "dashboards",
-#                             "mountPath": "/etc/grafana/provisioning/dashboards/",
-#                             "files": [
-#                                 {
-#                                     "path": "dashboard-osm.yml",
-#                                     "content": (
-#                                         "apiVersion: 1\n"
-#                                         "providers:\n"
-#                                         "  - name: 'osm'\n"
-#                                         "    orgId: 1\n"
-#                                         "    folder: ''\n"
-#                                         "    type: file\n"
-#                                         "    options:\n"
-#                                         "      path: /etc/grafana/provisioning/dashboards/\n"
-#                                     ),
-#                                 },
-#                             ],
-#                         },
-#                         {
-#                             "name": "datasources",
-#                             "mountPath": "/etc/grafana/provisioning/datasources/",
-#                             "files": [
-#                                 {
-#                                     "path": "datasource-prometheus.yml",
-#                                     "content": (
-#                                         "datasources:\n"
-#                                         "  - access: proxy\n"
-#                                         "    editable: true\n"
-#                                         "    is_default: true\n"
-#                                         "    name: osm_prometheus\n"
-#                                         "    orgId: 1\n"
-#                                         "    type: prometheus\n"
-#                                         "    version: 1\n"
-#                                         "    url: http://prometheus:9090\n"
-#                                     ),
-#                                 },
-#                             ],
-#                         },
-#                     ],
-#                     "kubernetes": {
-#                         "readinessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": 3000,
-#                             },
-#                             "initialDelaySeconds": 10,
-#                             "periodSeconds": 10,
-#                             "timeoutSeconds": 5,
-#                             "successThreshold": 1,
-#                             "failureThreshold": 3,
-#                         },
-#                         "livenessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": 3000,
-#                             },
-#                             "initialDelaySeconds": 60,
-#                             "timeoutSeconds": 30,
-#                             "failureThreshold": 10,
-#                         },
-#                     },
-#                 },
-#             ],
-#             "kubernetesResources": {
-#                 "ingressResources": [
-#                     {
-#                         "name": "grafana-ingress",
-#                         "annotations": {
-#                             "nginx.ingress.kubernetes.io/proxy-body-size": "0",
-#                             "nginx.ingress.kubernetes.io/whitelist-source-range": "0.0.0.0/0",
-#                         },
-#                         "spec": {
-#                             "rules": [
-#                                 {
-#                                     "host": "grafana",
-#                                     "http": {
-#                                         "paths": [
-#                                             {
-#                                                 "path": "/",
-#                                                 "backend": {
-#                                                     "serviceName": "grafana",
-#                                                     "servicePort": 3000,
-#                                                 },
-#                                             }
-#                                         ]
-#                                     },
-#                                 }
-#                             ],
-#                             "tls": [{"hosts": ["grafana"], "secretName": "grafana"}],
-#                         },
-#                     }
-#                 ],
-#             },
-#         }
-
-#         self.harness.charm.on.start.emit()
-
-#         # Initializing the prometheus relation
-#         relation_id = self.harness.add_relation("prometheus", "prometheus")
-#         self.harness.add_relation_unit(relation_id, "prometheus/0")
-#         self.harness.update_relation_data(
-#             relation_id,
-#             "prometheus",
-#             {
-#                 "hostname": "prometheus",
-#                 "port": "9090",
-#             },
-#         )
-
-#         self.harness.update_config(
-#             {
-#                 "site_url": "https://grafana",
-#                 "tls_secret_name": "grafana",
-#                 "ingress_whitelist_source_range": "0.0.0.0/0",
-#             }
-#         )
-
-#         pod_spec, _ = self.harness.get_pod_spec()
-
-#         self.assertDictEqual(expected_result, pod_spec)
-
-#     def test_on_prometheus_unit_relation_changed(self) -> NoReturn:
-#         """Test to see if prometheus relation is updated."""
-#         self.harness.charm.on.start.emit()
-
-#         relation_id = self.harness.add_relation("prometheus", "prometheus")
-#         self.harness.add_relation_unit(relation_id, "prometheus/0")
-#         self.harness.update_relation_data(
-#             relation_id,
-#             "prometheus",
-#             {"hostname": "prometheus", "port": 9090},
-#         )
-
-#         # Verifying status
-#         self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
-
-
-if __name__ == "__main__":
-    unittest.main()
diff --git a/installers/charm/grafana/tests/test_pod_spec.py b/installers/charm/grafana/tests/test_pod_spec.py
deleted file mode 100644 (file)
index 88c85d3..0000000
+++ /dev/null
@@ -1,636 +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.
-# #
-# # For those usages not covered by the Apache License, Version 2.0 please
-# # contact: legal@canonical.com
-# #
-# # To get in touch with the maintainers, please contact:
-# # osm-charmers@lists.launchpad.net
-# ##
-
-# from typing import NoReturn
-# import unittest
-
-# import pod_spec
-
-
-# class TestPodSpec(unittest.TestCase):
-#     """Pod spec unit tests."""
-
-#     def test_make_pod_ports(self) -> NoReturn:
-#         """Testing make pod ports."""
-#         port = 3000
-
-#         expected_result = [
-#             {
-#                 "name": "grafana",
-#                 "containerPort": port,
-#                 "protocol": "TCP",
-#             }
-#         ]
-
-#         pod_ports = pod_spec._make_pod_ports(port)
-
-#         self.assertListEqual(expected_result, pod_ports)
-
-#     def test_make_pod_envconfig(self) -> NoReturn:
-#         """Teting make pod envconfig."""
-#         config = {}
-#         relation_state = {
-#             "prometheus_hostname": "prometheus",
-#             "prometheus_port": "9090",
-#         }
-
-#         expected_result = {}
-
-#         pod_envconfig = pod_spec._make_pod_envconfig(config, relation_state)
-
-#         self.assertDictEqual(expected_result, pod_envconfig)
-
-#     def test_make_pod_ingress_resources_without_site_url(self) -> NoReturn:
-#         """Testing make pod ingress resources without site_url."""
-#         config = {"site_url": ""}
-#         app_name = "grafana"
-#         port = 3000
-
-#         pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-#             config, app_name, port
-#         )
-
-#         self.assertIsNone(pod_ingress_resources)
-
-#     def test_make_pod_ingress_resources(self) -> NoReturn:
-#         """Testing make pod ingress resources."""
-#         config = {
-#             "site_url": "http://grafana",
-#             "max_file_size": 0,
-#             "ingress_whitelist_source_range": "",
-#         }
-#         app_name = "grafana"
-#         port = 3000
-
-#         expected_result = [
-#             {
-#                 "name": f"{app_name}-ingress",
-#                 "annotations": {
-#                     "nginx.ingress.kubernetes.io/proxy-body-size": f"{config['max_file_size']}",
-#                     "nginx.ingress.kubernetes.io/ssl-redirect": "false",
-#                 },
-#                 "spec": {
-#                     "rules": [
-#                         {
-#                             "host": app_name,
-#                             "http": {
-#                                 "paths": [
-#                                     {
-#                                         "path": "/",
-#                                         "backend": {
-#                                             "serviceName": app_name,
-#                                             "servicePort": port,
-#                                         },
-#                                     }
-#                                 ]
-#                             },
-#                         }
-#                     ]
-#                 },
-#             }
-#         ]
-
-#         pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-#             config, app_name, port
-#         )
-
-#         self.assertListEqual(expected_result, pod_ingress_resources)
-
-#     def test_make_pod_ingress_resources_with_whitelist_source_range(self) -> NoReturn:
-#         """Testing make pod ingress resources with whitelist_source_range."""
-#         config = {
-#             "site_url": "http://grafana",
-#             "max_file_size": 0,
-#             "ingress_whitelist_source_range": "0.0.0.0/0",
-#         }
-#         app_name = "grafana"
-#         port = 3000
-
-#         expected_result = [
-#             {
-#                 "name": f"{app_name}-ingress",
-#                 "annotations": {
-#                     "nginx.ingress.kubernetes.io/proxy-body-size": f"{config['max_file_size']}",
-#                     "nginx.ingress.kubernetes.io/ssl-redirect": "false",
-#                     "nginx.ingress.kubernetes.io/whitelist-source-range": config[
-#                         "ingress_whitelist_source_range"
-#                     ],
-#                 },
-#                 "spec": {
-#                     "rules": [
-#                         {
-#                             "host": app_name,
-#                             "http": {
-#                                 "paths": [
-#                                     {
-#                                         "path": "/",
-#                                         "backend": {
-#                                             "serviceName": app_name,
-#                                             "servicePort": port,
-#                                         },
-#                                     }
-#                                 ]
-#                             },
-#                         }
-#                     ]
-#                 },
-#             }
-#         ]
-
-#         pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-#             config, app_name, port
-#         )
-
-#         self.assertListEqual(expected_result, pod_ingress_resources)
-
-#     def test_make_pod_ingress_resources_with_https(self) -> NoReturn:
-#         """Testing make pod ingress resources with HTTPs."""
-#         config = {
-#             "site_url": "https://grafana",
-#             "max_file_size": 0,
-#             "ingress_whitelist_source_range": "",
-#             "tls_secret_name": "",
-#         }
-#         app_name = "grafana"
-#         port = 3000
-
-#         expected_result = [
-#             {
-#                 "name": f"{app_name}-ingress",
-#                 "annotations": {
-#                     "nginx.ingress.kubernetes.io/proxy-body-size": f"{config['max_file_size']}",
-#                 },
-#                 "spec": {
-#                     "rules": [
-#                         {
-#                             "host": app_name,
-#                             "http": {
-#                                 "paths": [
-#                                     {
-#                                         "path": "/",
-#                                         "backend": {
-#                                             "serviceName": app_name,
-#                                             "servicePort": port,
-#                                         },
-#                                     }
-#                                 ]
-#                             },
-#                         }
-#                     ],
-#                     "tls": [{"hosts": [app_name]}],
-#                 },
-#             }
-#         ]
-
-#         pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-#             config, app_name, port
-#         )
-
-#         self.assertListEqual(expected_result, pod_ingress_resources)
-
-#     def test_make_pod_ingress_resources_with_https_tls_secret_name(self) -> NoReturn:
-#         """Testing make pod ingress resources with HTTPs and TLS secret name."""
-#         config = {
-#             "site_url": "https://grafana",
-#             "max_file_size": 0,
-#             "ingress_whitelist_source_range": "",
-#             "tls_secret_name": "secret_name",
-#         }
-#         app_name = "grafana"
-#         port = 3000
-
-#         expected_result = [
-#             {
-#                 "name": f"{app_name}-ingress",
-#                 "annotations": {
-#                     "nginx.ingress.kubernetes.io/proxy-body-size": f"{config['max_file_size']}",
-#                 },
-#                 "spec": {
-#                     "rules": [
-#                         {
-#                             "host": app_name,
-#                             "http": {
-#                                 "paths": [
-#                                     {
-#                                         "path": "/",
-#                                         "backend": {
-#                                             "serviceName": app_name,
-#                                             "servicePort": port,
-#                                         },
-#                                     }
-#                                 ]
-#                             },
-#                         }
-#                     ],
-#                     "tls": [
-#                         {"hosts": [app_name], "secretName": config["tls_secret_name"]}
-#                     ],
-#                 },
-#             }
-#         ]
-
-#         pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-#             config, app_name, port
-#         )
-
-#         self.assertListEqual(expected_result, pod_ingress_resources)
-
-#     def test_make_pod_files(self) -> NoReturn:
-#         """Testing make pod files."""
-#         config = {"osm_dashboards": False}
-#         relation_state = {
-#             "prometheus_hostname": "prometheus",
-#             "prometheus_port": "9090",
-#         }
-
-#         expected_result = [
-#             {
-#                 "name": "dashboards",
-#                 "mountPath": "/etc/grafana/provisioning/dashboards/",
-#                 "files": [
-#                     {
-#                         "path": "dashboard-osm.yml",
-#                         "content": (
-#                             "apiVersion: 1\n"
-#                             "providers:\n"
-#                             "  - name: 'osm'\n"
-#                             "    orgId: 1\n"
-#                             "    folder: ''\n"
-#                             "    type: file\n"
-#                             "    options:\n"
-#                             "      path: /etc/grafana/provisioning/dashboards/\n"
-#                         ),
-#                     }
-#                 ],
-#             },
-#             {
-#                 "name": "datasources",
-#                 "mountPath": "/etc/grafana/provisioning/datasources/",
-#                 "files": [
-#                     {
-#                         "path": "datasource-prometheus.yml",
-#                         "content": (
-#                             "datasources:\n"
-#                             "  - access: proxy\n"
-#                             "    editable: true\n"
-#                             "    is_default: true\n"
-#                             "    name: osm_prometheus\n"
-#                             "    orgId: 1\n"
-#                             "    type: prometheus\n"
-#                             "    version: 1\n"
-#                             "    url: http://{}:{}\n".format(
-#                                 relation_state.get("prometheus_hostname"),
-#                                 relation_state.get("prometheus_port"),
-#                             )
-#                         ),
-#                     }
-#                 ],
-#             },
-#         ]
-
-#         pod_envconfig = pod_spec._make_pod_files(config, relation_state)
-#         self.assertListEqual(expected_result, pod_envconfig)
-
-#     def test_make_readiness_probe(self) -> NoReturn:
-#         """Testing make readiness probe."""
-#         port = 3000
-
-#         expected_result = {
-#             "httpGet": {
-#                 "path": "/api/health",
-#                 "port": port,
-#             },
-#             "initialDelaySeconds": 10,
-#             "periodSeconds": 10,
-#             "timeoutSeconds": 5,
-#             "successThreshold": 1,
-#             "failureThreshold": 3,
-#         }
-
-#         readiness_probe = pod_spec._make_readiness_probe(port)
-
-#         self.assertDictEqual(expected_result, readiness_probe)
-
-#     def test_make_liveness_probe(self) -> NoReturn:
-#         """Testing make liveness probe."""
-#         port = 3000
-
-#         expected_result = {
-#             "httpGet": {
-#                 "path": "/api/health",
-#                 "port": port,
-#             },
-#             "initialDelaySeconds": 60,
-#             "timeoutSeconds": 30,
-#             "failureThreshold": 10,
-#         }
-
-#         liveness_probe = pod_spec._make_liveness_probe(port)
-
-#         self.assertDictEqual(expected_result, liveness_probe)
-
-#     def test_make_pod_spec(self) -> NoReturn:
-#         """Testing make pod spec."""
-#         image_info = {"upstream-source": "ubuntu/grafana:latest"}
-#         config = {
-#             "site_url": "",
-#         }
-#         relation_state = {
-#             "prometheus_hostname": "prometheus",
-#             "prometheus_port": "9090",
-#         }
-#         app_name = "grafana"
-#         port = 3000
-
-#         expected_result = {
-#             "version": 3,
-#             "containers": [
-#                 {
-#                     "name": app_name,
-#                     "imageDetails": image_info,
-#                     "imagePullPolicy": "Always",
-#                     "ports": [
-#                         {
-#                             "name": app_name,
-#                             "containerPort": port,
-#                             "protocol": "TCP",
-#                         }
-#                     ],
-#                     "envConfig": {},
-#                     "volumeConfig": [
-#                         {
-#                             "name": "dashboards",
-#                             "mountPath": "/etc/grafana/provisioning/dashboards/",
-#                             "files": [
-#                                 {
-#                                     "path": "dashboard-osm.yml",
-#                                     "content": (
-#                                         "apiVersion: 1\n"
-#                                         "providers:\n"
-#                                         "  - name: 'osm'\n"
-#                                         "    orgId: 1\n"
-#                                         "    folder: ''\n"
-#                                         "    type: file\n"
-#                                         "    options:\n"
-#                                         "      path: /etc/grafana/provisioning/dashboards/\n"
-#                                     ),
-#                                 }
-#                             ],
-#                         },
-#                         {
-#                             "name": "datasources",
-#                             "mountPath": "/etc/grafana/provisioning/datasources/",
-#                             "files": [
-#                                 {
-#                                     "path": "datasource-prometheus.yml",
-#                                     "content": (
-#                                         "datasources:\n"
-#                                         "  - access: proxy\n"
-#                                         "    editable: true\n"
-#                                         "    is_default: true\n"
-#                                         "    name: osm_prometheus\n"
-#                                         "    orgId: 1\n"
-#                                         "    type: prometheus\n"
-#                                         "    version: 1\n"
-#                                         "    url: http://{}:{}\n".format(
-#                                             relation_state.get("prometheus_hostname"),
-#                                             relation_state.get("prometheus_port"),
-#                                         )
-#                                     ),
-#                                 }
-#                             ],
-#                         },
-#                     ],
-#                     "kubernetes": {
-#                         "readinessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": port,
-#                             },
-#                             "initialDelaySeconds": 10,
-#                             "periodSeconds": 10,
-#                             "timeoutSeconds": 5,
-#                             "successThreshold": 1,
-#                             "failureThreshold": 3,
-#                         },
-#                         "livenessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": port,
-#                             },
-#                             "initialDelaySeconds": 60,
-#                             "timeoutSeconds": 30,
-#                             "failureThreshold": 10,
-#                         },
-#                     },
-#                 }
-#             ],
-#             "kubernetesResources": {"ingressResources": []},
-#         }
-
-#         spec = pod_spec.make_pod_spec(
-#             image_info, config, relation_state, app_name, port
-#         )
-
-#         self.assertDictEqual(expected_result, spec)
-
-#     def test_make_pod_spec_with_ingress(self) -> NoReturn:
-#         """Testing make pod spec."""
-#         image_info = {"upstream-source": "ubuntu/grafana:latest"}
-#         config = {
-#             "site_url": "https://grafana",
-#             "tls_secret_name": "grafana",
-#             "max_file_size": 0,
-#             "ingress_whitelist_source_range": "0.0.0.0/0",
-#         }
-#         relation_state = {
-#             "prometheus_hostname": "prometheus",
-#             "prometheus_port": "9090",
-#         }
-#         app_name = "grafana"
-#         port = 3000
-
-#         expected_result = {
-#             "version": 3,
-#             "containers": [
-#                 {
-#                     "name": app_name,
-#                     "imageDetails": image_info,
-#                     "imagePullPolicy": "Always",
-#                     "ports": [
-#                         {
-#                             "name": app_name,
-#                             "containerPort": port,
-#                             "protocol": "TCP",
-#                         }
-#                     ],
-#                     "envConfig": {},
-#                     "volumeConfig": [
-#                         {
-#                             "name": "dashboards",
-#                             "mountPath": "/etc/grafana/provisioning/dashboards/",
-#                             "files": [
-#                                 {
-#                                     "path": "dashboard-osm.yml",
-#                                     "content": (
-#                                         "apiVersion: 1\n"
-#                                         "providers:\n"
-#                                         "  - name: 'osm'\n"
-#                                         "    orgId: 1\n"
-#                                         "    folder: ''\n"
-#                                         "    type: file\n"
-#                                         "    options:\n"
-#                                         "      path: /etc/grafana/provisioning/dashboards/\n"
-#                                     ),
-#                                 }
-#                             ],
-#                         },
-#                         {
-#                             "name": "datasources",
-#                             "mountPath": "/etc/grafana/provisioning/datasources/",
-#                             "files": [
-#                                 {
-#                                     "path": "datasource-prometheus.yml",
-#                                     "content": (
-#                                         "datasources:\n"
-#                                         "  - access: proxy\n"
-#                                         "    editable: true\n"
-#                                         "    is_default: true\n"
-#                                         "    name: osm_prometheus\n"
-#                                         "    orgId: 1\n"
-#                                         "    type: prometheus\n"
-#                                         "    version: 1\n"
-#                                         "    url: http://{}:{}\n".format(
-#                                             relation_state.get("prometheus_hostname"),
-#                                             relation_state.get("prometheus_port"),
-#                                         )
-#                                     ),
-#                                 }
-#                             ],
-#                         },
-#                     ],
-#                     "kubernetes": {
-#                         "readinessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": port,
-#                             },
-#                             "initialDelaySeconds": 10,
-#                             "periodSeconds": 10,
-#                             "timeoutSeconds": 5,
-#                             "successThreshold": 1,
-#                             "failureThreshold": 3,
-#                         },
-#                         "livenessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": port,
-#                             },
-#                             "initialDelaySeconds": 60,
-#                             "timeoutSeconds": 30,
-#                             "failureThreshold": 10,
-#                         },
-#                     },
-#                 }
-#             ],
-#             "kubernetesResources": {
-#                 "ingressResources": [
-#                     {
-#                         "name": "{}-ingress".format(app_name),
-#                         "annotations": {
-#                             "nginx.ingress.kubernetes.io/proxy-body-size": str(
-#                                 config.get("max_file_size")
-#                             ),
-#                             "nginx.ingress.kubernetes.io/whitelist-source-range": config.get(
-#                                 "ingress_whitelist_source_range"
-#                             ),
-#                         },
-#                         "spec": {
-#                             "rules": [
-#                                 {
-#                                     "host": app_name,
-#                                     "http": {
-#                                         "paths": [
-#                                             {
-#                                                 "path": "/",
-#                                                 "backend": {
-#                                                     "serviceName": app_name,
-#                                                     "servicePort": port,
-#                                                 },
-#                                             }
-#                                         ]
-#                                     },
-#                                 }
-#                             ],
-#                             "tls": [
-#                                 {
-#                                     "hosts": [app_name],
-#                                     "secretName": config.get("tls_secret_name"),
-#                                 }
-#                             ],
-#                         },
-#                     }
-#                 ],
-#             },
-#         }
-
-#         spec = pod_spec.make_pod_spec(
-#             image_info, config, relation_state, app_name, port
-#         )
-
-#         self.assertDictEqual(expected_result, spec)
-
-#     def test_make_pod_spec_without_image_info(self) -> NoReturn:
-#         """Testing make pod spec without image_info."""
-#         image_info = None
-#         config = {
-#             "site_url": "",
-#         }
-#         relation_state = {
-#             "prometheus_hostname": "prometheus",
-#             "prometheus_port": "9090",
-#         }
-#         app_name = "grafana"
-#         port = 3000
-
-#         spec = pod_spec.make_pod_spec(
-#             image_info, config, relation_state, app_name, port
-#         )
-
-#         self.assertIsNone(spec)
-
-#     def test_make_pod_spec_without_relation_state(self) -> NoReturn:
-#         """Testing make pod spec without relation_state."""
-#         image_info = {"upstream-source": "ubuntu/grafana:latest"}
-#         config = {
-#             "site_url": "",
-#         }
-#         relation_state = {}
-#         app_name = "grafana"
-#         port = 3000
-
-#         with self.assertRaises(ValueError):
-#             pod_spec.make_pod_spec(image_info, config, relation_state, app_name, port)
-
-
-# if __name__ == "__main__":
-#     unittest.main()
diff --git a/installers/charm/grafana/tox.ini b/installers/charm/grafana/tox.ini
deleted file mode 100644 (file)
index 58e13a6..0000000
+++ /dev/null
@@ -1,126 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-#######################################################################################
-
-[tox]
-envlist = black, cover, flake8, pylint, yamllint, safety
-skipsdist = true
-
-[tox:jenkins]
-toxworkdir = /tmp/.tox
-
-[testenv]
-basepython = python3.8
-setenv = VIRTUAL_ENV={envdir}
-         PYTHONDONTWRITEBYTECODE = 1
-deps =  -r{toxinidir}/requirements.txt
-
-
-#######################################################################################
-[testenv:black]
-deps = black
-commands =
-        black --check --diff src/ tests/
-
-
-#######################################################################################
-[testenv:cover]
-deps =  {[testenv]deps}
-        -r{toxinidir}/requirements-test.txt
-        coverage
-        nose2
-commands =
-        sh -c 'rm -f nosetests.xml'
-        coverage erase
-        nose2 -C --coverage src
-        coverage report --omit='*tests*'
-        coverage html -d ./cover --omit='*tests*'
-        coverage xml -o coverage.xml --omit=*tests*
-whitelist_externals = sh
-
-
-#######################################################################################
-[testenv:flake8]
-deps =  flake8
-        flake8-import-order
-commands =
-        flake8 src/ tests/ --exclude=*pod_spec*
-
-
-#######################################################################################
-[testenv:pylint]
-deps =  {[testenv]deps}
-        -r{toxinidir}/requirements-test.txt
-        pylint==2.10.2
-commands =
-    pylint -E src/ tests/
-
-
-#######################################################################################
-[testenv:safety]
-setenv =
-        LC_ALL=C.UTF-8
-        LANG=C.UTF-8
-deps =  {[testenv]deps}
-        safety
-commands =
-        - safety check --full-report
-
-
-#######################################################################################
-[testenv:yamllint]
-deps =  {[testenv]deps}
-        -r{toxinidir}/requirements-test.txt
-        yamllint
-commands = yamllint .
-
-#######################################################################################
-[testenv:build]
-passenv=HTTP_PROXY HTTPS_PROXY NO_PROXY
-whitelist_externals =
-  charmcraft
-  sh
-commands =
-  charmcraft pack
-  sh -c 'ubuntu_version=20.04; \
-        architectures="amd64-aarch64-arm64"; \
-        charm_name=`cat metadata.yaml | grep -E "^name: " | cut -f 2 -d " "`; \
-        mv $charm_name"_ubuntu-"$ubuntu_version-$architectures.charm $charm_name.charm'
-
-#######################################################################################
-[flake8]
-ignore =
-        W291,
-        W293,
-        W503,
-        E123,
-        E125,
-        E226,
-        E241,
-exclude =
-        .git,
-        __pycache__,
-        .tox,
-max-line-length = 120
-show-source = True
-builtins = _
-max-complexity = 10
-import-order-style = google
diff --git a/installers/charm/juju-simplestreams-operator/.gitignore b/installers/charm/juju-simplestreams-operator/.gitignore
deleted file mode 100644 (file)
index 87d0a58..0000000
+++ /dev/null
@@ -1,29 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-venv/
-build/
-*.charm
-.tox/
-.coverage
-coverage.xml
-__pycache__/
-*.py[cod]
-.vscode
\ No newline at end of file
diff --git a/installers/charm/juju-simplestreams-operator/.jujuignore b/installers/charm/juju-simplestreams-operator/.jujuignore
deleted file mode 100644 (file)
index 17c7a8b..0000000
+++ /dev/null
@@ -1,23 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-/venv
-*.py[cod]
-*.charm
diff --git a/installers/charm/juju-simplestreams-operator/CONTRIBUTING.md b/installers/charm/juju-simplestreams-operator/CONTRIBUTING.md
deleted file mode 100644 (file)
index 74a6d6d..0000000
+++ /dev/null
@@ -1,78 +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.
-
-For those usages not covered by the Apache License, Version 2.0 please
-contact: legal@canonical.com
-
-To get in touch with the maintainers, please contact:
-osm-charmers@lists.launchpad.net -->
-
-# Contributing
-
-## Overview
-
-This documents explains the processes and practices recommended for contributing enhancements to
-this operator.
-
-- Generally, before developing enhancements to this charm, you should consider [opening an issue
-  ](https://osm.etsi.org/bugzilla/enter_bug.cgi?product=OSM) explaining your use case. (Component=devops, version=master)
-- If you would like to chat with us about your use-cases or proposed implementation, you can reach
-  us at [OSM Juju public channel](https://opensourcemano.slack.com/archives/C027KJGPECA).
-- 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 gerrit patch onto
-  the `master` branch.
-
-## 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 dev
-# Enable DEBUG logging
-juju model-config logging-config="<root>=INFO;unit=DEBUG"
-# Deploy the charm
-juju deploy ./osm-juju-simplestreams_ubuntu-22.04-amd64.charm \
-    --resource server-image=nginx:1.23.0
-```
diff --git a/installers/charm/juju-simplestreams-operator/LICENSE b/installers/charm/juju-simplestreams-operator/LICENSE
deleted file mode 100644 (file)
index 7e9d504..0000000
+++ /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 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.
diff --git a/installers/charm/juju-simplestreams-operator/README.md b/installers/charm/juju-simplestreams-operator/README.md
deleted file mode 100644 (file)
index bc94dde..0000000
+++ /dev/null
@@ -1,42 +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.
-
-For those usages not covered by the Apache License, Version 2.0 please
-contact: legal@canonical.com
-
-To get in touch with the maintainers, please contact:
-osm-charmers@lists.launchpad.net -->
-
-<!-- 
-Avoid using this README file for information that is maintained or published elsewhere, e.g.:
-
-* metadata.yaml > published on Charmhub
-* documentation > published on (or linked to from) Charmhub
-* detailed contribution guide > documentation or CONTRIBUTING.md
-
-Use links instead. 
--->
-
-# Juju simplestreams
-
-Charmhub package name: osm-juju-simplestreams
-More information: https://charmhub.io/osm-juju-simplestreams
-
-## Other resources
-
-* [Read more](https://osm.etsi.org/docs/user-guide/latest/) 
-
-* [Contributing](https://osm.etsi.org/gitweb/?p=osm/devops.git;a=blob;f=installers/charm/osm-juju-simplestreams/CONTRIBUTING.md)
-
-* See the [Juju SDK documentation](https://juju.is/docs/sdk) for more information about developing and improving charms.
diff --git a/installers/charm/juju-simplestreams-operator/actions.yaml b/installers/charm/juju-simplestreams-operator/actions.yaml
deleted file mode 100644 (file)
index c8d0e32..0000000
+++ /dev/null
@@ -1,48 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Actions tab on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
-
-add-image-metadata:
-  description: Action to add image metadata
-  params:
-    series:
-      description: Charm series
-      type: string
-    image-id:
-      description: Openstack image id for the specified series
-      type: string
-    region:
-      description: Openstack region
-      type: string
-    auth-url:
-      description: Openstack authentication url
-      type: string
-  required:
-    - series
-    - image-id
-    - region
-    - auth-url
-backup:
-  description: Action to get a backup of the important data.
-restore:
-  description: Action to restore from a backup.
diff --git a/installers/charm/juju-simplestreams-operator/charmcraft.yaml b/installers/charm/juju-simplestreams-operator/charmcraft.yaml
deleted file mode 100644 (file)
index f8944c5..0000000
+++ /dev/null
@@ -1,34 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-
-type: charm
-bases:
-  - build-on:
-      - name: "ubuntu"
-        channel: "20.04"
-    run-on:
-      - name: "ubuntu"
-        channel: "20.04"
-
-parts:
-  charm:
-    prime:
-      - files/*
diff --git a/installers/charm/juju-simplestreams-operator/config.yaml b/installers/charm/juju-simplestreams-operator/config.yaml
deleted file mode 100644 (file)
index b76533f..0000000
+++ /dev/null
@@ -1,51 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Configure tab on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
-
-options:
-  # Ingress options
-  external-hostname:
-    default: ""
-    description: |
-      The url that will be configured in the Kubernetes ingress.
-
-      The easiest way of configuring the external-hostname without having the DNS setup is by using
-      a Wildcard DNS like nip.io constructing the url like so:
-        - nbi.127.0.0.1.nip.io (valid within the K8s cluster node)
-        - nbi.<k8s-worker-ip>.nip.io (valid from outside the K8s cluster node)
-
-      This option is only applicable when the Kubernetes cluster has nginx ingress configured
-      and the charm is related to the nginx-ingress-integrator.
-      See more: https://charmhub.io/nginx-ingress-integrator
-    type: string
-  max-body-size:
-    default: 20
-    description:
-      Max allowed body-size (for file uploads) in megabytes, set to 0 to
-      disable limits.
-    source: default
-    type: int
-    value: 20
-  tls-secret-name:
-    description: TLS secret name to use for ingress.
-    type: string
diff --git a/installers/charm/juju-simplestreams-operator/files/juju-metadata b/installers/charm/juju-simplestreams-operator/files/juju-metadata
deleted file mode 100755 (executable)
index b6007fe..0000000
Binary files a/installers/charm/juju-simplestreams-operator/files/juju-metadata and /dev/null differ
diff --git a/installers/charm/juju-simplestreams-operator/files/nginx.conf b/installers/charm/juju-simplestreams-operator/files/nginx.conf
deleted file mode 100644 (file)
index d47540e..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-#######################################################################################
-# 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.
-#######################################################################################
-
-events {}
-http {
-    include mime.types;
-    sendfile on;
-
-    server {
-        listen 8080;
-        listen [::]:8080;
-
-        autoindex off;
-
-        server_name _;
-        server_tokens off;
-
-        root /app/static;
-        gzip_static on;
-    }
-}
\ No newline at end of file
diff --git a/installers/charm/juju-simplestreams-operator/lib/charms/nginx_ingress_integrator/v0/ingress.py b/installers/charm/juju-simplestreams-operator/lib/charms/nginx_ingress_integrator/v0/ingress.py
deleted file mode 100644 (file)
index be2d762..0000000
+++ /dev/null
@@ -1,229 +0,0 @@
-# See LICENSE file for licensing details.
-#   http://www.apache.org/licenses/LICENSE-2.0
-"""Library for the ingress relation.
-
-This library contains the Requires and Provides classes for handling
-the ingress interface.
-
-Import `IngressRequires` in your charm, with two required options:
-    - "self" (the charm itself)
-    - config_dict
-
-`config_dict` accepts the following keys:
-    - service-hostname (required)
-    - service-name (required)
-    - service-port (required)
-    - additional-hostnames
-    - limit-rps
-    - limit-whitelist
-    - max-body-size
-    - owasp-modsecurity-crs
-    - path-routes
-    - retry-errors
-    - rewrite-enabled
-    - rewrite-target
-    - service-namespace
-    - session-cookie-max-age
-    - tls-secret-name
-
-See [the config section](https://charmhub.io/nginx-ingress-integrator/configure) for descriptions
-of each, along with the required type.
-
-As an example, add the following to `src/charm.py`:
-```
-from charms.nginx_ingress_integrator.v0.ingress import IngressRequires
-
-# In your charm's `__init__` method.
-self.ingress = IngressRequires(self, {"service-hostname": self.config["external_hostname"],
-                                      "service-name": self.app.name,
-                                      "service-port": 80})
-
-# In your charm's `config-changed` handler.
-self.ingress.update_config({"service-hostname": self.config["external_hostname"]})
-```
-And then add the following to `metadata.yaml`:
-```
-requires:
-  ingress:
-    interface: ingress
-```
-You _must_ register the IngressRequires class as part of the `__init__` method
-rather than, for instance, a config-changed event handler. This is because
-doing so won't get the current relation changed event, because it wasn't
-registered to handle the event (because it wasn't created in `__init__` when
-the event was fired).
-"""
-
-import logging
-
-from ops.charm import CharmEvents
-from ops.framework import EventBase, EventSource, Object
-from ops.model import BlockedStatus
-
-# The unique Charmhub library identifier, never change it
-LIBID = "db0af4367506491c91663468fb5caa4c"
-
-# 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 = 10
-
-logger = logging.getLogger(__name__)
-
-REQUIRED_INGRESS_RELATION_FIELDS = {
-    "service-hostname",
-    "service-name",
-    "service-port",
-}
-
-OPTIONAL_INGRESS_RELATION_FIELDS = {
-    "additional-hostnames",
-    "limit-rps",
-    "limit-whitelist",
-    "max-body-size",
-    "owasp-modsecurity-crs",
-    "path-routes",
-    "retry-errors",
-    "rewrite-target",
-    "rewrite-enabled",
-    "service-namespace",
-    "session-cookie-max-age",
-    "tls-secret-name",
-}
-
-
-class IngressAvailableEvent(EventBase):
-    pass
-
-
-class IngressBrokenEvent(EventBase):
-    pass
-
-
-class IngressCharmEvents(CharmEvents):
-    """Custom charm events."""
-
-    ingress_available = EventSource(IngressAvailableEvent)
-    ingress_broken = EventSource(IngressBrokenEvent)
-
-
-class IngressRequires(Object):
-    """This class defines the functionality for the 'requires' side of the 'ingress' relation.
-
-    Hook events observed:
-        - relation-changed
-    """
-
-    def __init__(self, charm, config_dict):
-        super().__init__(charm, "ingress")
-
-        self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed)
-
-        self.config_dict = config_dict
-
-    def _config_dict_errors(self, update_only=False):
-        """Check our config dict for errors."""
-        blocked_message = "Error in ingress relation, check `juju debug-log`"
-        unknown = [
-            x
-            for x in self.config_dict
-            if x not in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
-        ]
-        if unknown:
-            logger.error(
-                "Ingress relation error, unknown key(s) in config dictionary found: %s",
-                ", ".join(unknown),
-            )
-            self.model.unit.status = BlockedStatus(blocked_message)
-            return True
-        if not update_only:
-            missing = [x for x in REQUIRED_INGRESS_RELATION_FIELDS if x not in self.config_dict]
-            if missing:
-                logger.error(
-                    "Ingress relation error, missing required key(s) in config dictionary: %s",
-                    ", ".join(sorted(missing)),
-                )
-                self.model.unit.status = BlockedStatus(blocked_message)
-                return True
-        return False
-
-    def _on_relation_changed(self, event):
-        """Handle the relation-changed event."""
-        # `self.unit` isn't available here, so use `self.model.unit`.
-        if self.model.unit.is_leader():
-            if self._config_dict_errors():
-                return
-            for key in self.config_dict:
-                event.relation.data[self.model.app][key] = str(self.config_dict[key])
-
-    def update_config(self, config_dict):
-        """Allow for updates to relation."""
-        if self.model.unit.is_leader():
-            self.config_dict = config_dict
-            if self._config_dict_errors(update_only=True):
-                return
-            relation = self.model.get_relation("ingress")
-            if relation:
-                for key in self.config_dict:
-                    relation.data[self.model.app][key] = str(self.config_dict[key])
-
-
-class IngressProvides(Object):
-    """This class defines the functionality for the 'provides' side of the 'ingress' relation.
-
-    Hook events observed:
-        - relation-changed
-    """
-
-    def __init__(self, charm):
-        super().__init__(charm, "ingress")
-        # Observe the relation-changed hook event and bind
-        # self.on_relation_changed() to handle the event.
-        self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed)
-        self.framework.observe(charm.on["ingress"].relation_broken, self._on_relation_broken)
-        self.charm = charm
-
-    def _on_relation_changed(self, event):
-        """Handle a change to the ingress relation.
-
-        Confirm we have the fields we expect to receive."""
-        # `self.unit` isn't available here, so use `self.model.unit`.
-        if not self.model.unit.is_leader():
-            return
-
-        ingress_data = {
-            field: event.relation.data[event.app].get(field)
-            for field in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
-        }
-
-        missing_fields = sorted(
-            [
-                field
-                for field in REQUIRED_INGRESS_RELATION_FIELDS
-                if ingress_data.get(field) is None
-            ]
-        )
-
-        if missing_fields:
-            logger.error(
-                "Missing required data fields for ingress relation: {}".format(
-                    ", ".join(missing_fields)
-                )
-            )
-            self.model.unit.status = BlockedStatus(
-                "Missing fields for ingress: {}".format(", ".join(missing_fields))
-            )
-
-        # Create an event that our charm can use to decide it's okay to
-        # configure the ingress.
-        self.charm.on.ingress_available.emit()
-
-    def _on_relation_broken(self, _):
-        """Handle a relation-broken event in the ingress relation."""
-        if not self.model.unit.is_leader():
-            return
-
-        # Create an event that our charm can use to remove the ingress resource.
-        self.charm.on.ingress_broken.emit()
diff --git a/installers/charm/juju-simplestreams-operator/lib/charms/observability_libs/v1/kubernetes_service_patch.py b/installers/charm/juju-simplestreams-operator/lib/charms/observability_libs/v1/kubernetes_service_patch.py
deleted file mode 100644 (file)
index 506dbf0..0000000
+++ /dev/null
@@ -1,291 +0,0 @@
-# Copyright 2021 Canonical Ltd.
-# See LICENSE file for licensing details.
-#   http://www.apache.org/licenses/LICENSE-2.0
-
-"""# KubernetesServicePatch Library.
-
-This library is designed to enable developers to more simply patch the Kubernetes Service created
-by Juju during the deployment of a sidecar charm. When sidecar charms are deployed, Juju creates a
-service named after the application in the namespace (named after the Juju model). This service by
-default contains a "placeholder" port, which is 65536/TCP.
-
-When modifying the default set of resources managed by Juju, one must consider the lifecycle of the
-charm. In this case, any modifications to the default service (created during deployment), will be
-overwritten during a charm upgrade.
-
-When initialised, this library binds a handler to the parent charm's `install` and `upgrade_charm`
-events which applies the patch to the cluster. This should ensure that the service ports are
-correct throughout the charm's life.
-
-The constructor simply takes a reference to the parent charm, and a list of
-[`lightkube`](https://github.com/gtsystem/lightkube) ServicePorts that each define a port for the
-service. For information regarding the `lightkube` `ServicePort` model, please visit the
-`lightkube` [docs](https://gtsystem.github.io/lightkube-models/1.23/models/core_v1/#serviceport).
-
-Optionally, a name of the service (in case service name needs to be patched as well), labels,
-selectors, and annotations can be provided as keyword arguments.
-
-## Getting Started
-
-To get started using the library, you just need to fetch the library using `charmcraft`. **Note
-that you also need to add `lightkube` and `lightkube-models` to your charm's `requirements.txt`.**
-
-```shell
-cd some-charm
-charmcraft fetch-lib charms.observability_libs.v0.kubernetes_service_patch
-echo <<-EOF >> requirements.txt
-lightkube
-lightkube-models
-EOF
-```
-
-Then, to initialise the library:
-
-For `ClusterIP` services:
-
-```python
-# ...
-from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
-from lightkube.models.core_v1 import ServicePort
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    port = ServicePort(443, name=f"{self.app.name}")
-    self.service_patcher = KubernetesServicePatch(self, [port])
-    # ...
-```
-
-For `LoadBalancer`/`NodePort` services:
-
-```python
-# ...
-from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
-from lightkube.models.core_v1 import ServicePort
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    port = ServicePort(443, name=f"{self.app.name}", targetPort=443, nodePort=30666)
-    self.service_patcher = KubernetesServicePatch(
-        self, [port], "LoadBalancer"
-    )
-    # ...
-```
-
-Port protocols can also be specified. Valid protocols are `"TCP"`, `"UDP"`, and `"SCTP"`
-
-```python
-# ...
-from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
-from lightkube.models.core_v1 import ServicePort
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    tcp = ServicePort(443, name=f"{self.app.name}-tcp", protocol="TCP")
-    udp = ServicePort(443, name=f"{self.app.name}-udp", protocol="UDP")
-    sctp = ServicePort(443, name=f"{self.app.name}-sctp", protocol="SCTP")
-    self.service_patcher = KubernetesServicePatch(self, [tcp, udp, sctp])
-    # ...
-```
-
-Additionally, you may wish to use mocks in your charm's unit testing to ensure that the library
-does not try to make any API calls, or open any files during testing that are unlikely to be
-present, and could break your tests. The easiest way to do this is during your test `setUp`:
-
-```python
-# ...
-
-@patch("charm.KubernetesServicePatch", lambda x, y: None)
-def setUp(self, *unused):
-    self.harness = Harness(SomeCharm)
-    # ...
-```
-"""
-
-import logging
-from types import MethodType
-from typing import List, Literal
-
-from lightkube import ApiError, Client
-from lightkube.models.core_v1 import ServicePort, ServiceSpec
-from lightkube.models.meta_v1 import ObjectMeta
-from lightkube.resources.core_v1 import Service
-from lightkube.types import PatchType
-from ops.charm import CharmBase
-from ops.framework import Object
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0042f86d0a874435adef581806cddbbb"
-
-# 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 = 1
-
-ServiceType = Literal["ClusterIP", "LoadBalancer"]
-
-
-class KubernetesServicePatch(Object):
-    """A utility for patching the Kubernetes service set up by Juju."""
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        ports: List[ServicePort],
-        service_name: str = None,
-        service_type: ServiceType = "ClusterIP",
-        additional_labels: dict = None,
-        additional_selectors: dict = None,
-        additional_annotations: dict = None,
-    ):
-        """Constructor for KubernetesServicePatch.
-
-        Args:
-            charm: the charm that is instantiating the library.
-            ports: a list of ServicePorts
-            service_name: allows setting custom name to the patched service. If none given,
-                application name will be used.
-            service_type: desired type of K8s service. Default value is in line with ServiceSpec's
-                default value.
-            additional_labels: Labels to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_selectors: Selectors to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_annotations: Annotations to be added to the kubernetes service.
-        """
-        super().__init__(charm, "kubernetes-service-patch")
-        self.charm = charm
-        self.service_name = service_name if service_name else self._app
-        self.service = self._service_object(
-            ports,
-            service_name,
-            service_type,
-            additional_labels,
-            additional_selectors,
-            additional_annotations,
-        )
-
-        # Make mypy type checking happy that self._patch is a method
-        assert isinstance(self._patch, MethodType)
-        # Ensure this patch is applied during the 'install' and 'upgrade-charm' events
-        self.framework.observe(charm.on.install, self._patch)
-        self.framework.observe(charm.on.upgrade_charm, self._patch)
-
-    def _service_object(
-        self,
-        ports: List[ServicePort],
-        service_name: str = None,
-        service_type: ServiceType = "ClusterIP",
-        additional_labels: dict = None,
-        additional_selectors: dict = None,
-        additional_annotations: dict = None,
-    ) -> Service:
-        """Creates a valid Service representation.
-
-        Args:
-            ports: a list of ServicePorts
-            service_name: allows setting custom name to the patched service. If none given,
-                application name will be used.
-            service_type: desired type of K8s service. Default value is in line with ServiceSpec's
-                default value.
-            additional_labels: Labels to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_selectors: Selectors to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_annotations: Annotations to be added to the kubernetes service.
-
-        Returns:
-            Service: A valid representation of a Kubernetes Service with the correct ports.
-        """
-        if not service_name:
-            service_name = self._app
-        labels = {"app.kubernetes.io/name": self._app}
-        if additional_labels:
-            labels.update(additional_labels)
-        selector = {"app.kubernetes.io/name": self._app}
-        if additional_selectors:
-            selector.update(additional_selectors)
-        return Service(
-            apiVersion="v1",
-            kind="Service",
-            metadata=ObjectMeta(
-                namespace=self._namespace,
-                name=service_name,
-                labels=labels,
-                annotations=additional_annotations,  # type: ignore[arg-type]
-            ),
-            spec=ServiceSpec(
-                selector=selector,
-                ports=ports,
-                type=service_type,
-            ),
-        )
-
-    def _patch(self, _) -> None:
-        """Patch the Kubernetes service created by Juju to map the correct port.
-
-        Raises:
-            PatchFailed: if patching fails due to lack of permissions, or otherwise.
-        """
-        if not self.charm.unit.is_leader():
-            return
-
-        client = Client()
-        try:
-            if self.service_name != self._app:
-                self._delete_and_create_service(client)
-            client.patch(Service, self.service_name, self.service, patch_type=PatchType.MERGE)
-        except ApiError as e:
-            if e.status.code == 403:
-                logger.error("Kubernetes service patch failed: `juju trust` this application.")
-            else:
-                logger.error("Kubernetes service patch failed: %s", str(e))
-        else:
-            logger.info("Kubernetes service '%s' patched successfully", self._app)
-
-    def _delete_and_create_service(self, client: Client):
-        service = client.get(Service, self._app, namespace=self._namespace)
-        service.metadata.name = self.service_name  # type: ignore[attr-defined]
-        service.metadata.resourceVersion = service.metadata.uid = None  # type: ignore[attr-defined]   # noqa: E501
-        client.delete(Service, self._app, namespace=self._namespace)
-        client.create(service)
-
-    def is_patched(self) -> bool:
-        """Reports if the service patch has been applied.
-
-        Returns:
-            bool: A boolean indicating if the service patch has been applied.
-        """
-        client = Client()
-        # Get the relevant service from the cluster
-        service = client.get(Service, name=self.service_name, namespace=self._namespace)
-        # Construct a list of expected ports, should the patch be applied
-        expected_ports = [(p.port, p.targetPort) for p in self.service.spec.ports]
-        # Construct a list in the same manner, using the fetched service
-        fetched_ports = [(p.port, p.targetPort) for p in service.spec.ports]  # type: ignore[attr-defined]  # noqa: E501
-        return expected_ports == fetched_ports
-
-    @property
-    def _app(self) -> str:
-        """Name of the current Juju application.
-
-        Returns:
-            str: A string containing the name of the current Juju application.
-        """
-        return self.charm.app.name
-
-    @property
-    def _namespace(self) -> str:
-        """The Kubernetes namespace we're running in.
-
-        Returns:
-            str: A string containing the name of the current Kubernetes namespace.
-        """
-        with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") as f:
-            return f.read().strip()
diff --git a/installers/charm/juju-simplestreams-operator/lib/charms/osm_libs/v0/utils.py b/installers/charm/juju-simplestreams-operator/lib/charms/osm_libs/v0/utils.py
deleted file mode 100644 (file)
index df3da94..0000000
+++ /dev/null
@@ -1,544 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-#         http://www.apache.org/licenses/LICENSE-2.0
-"""OSM Utils Library.
-
-This library offers some utilities made for but not limited to Charmed OSM.
-
-# Getting started
-
-Execute the following command inside your Charmed Operator folder to fetch the library.
-
-```shell
-charmcraft fetch-lib charms.osm_libs.v0.utils
-```
-
-# CharmError Exception
-
-An exception that takes to arguments, the message and the StatusBase class, which are useful
-to set the status of the charm when the exception raises.
-
-Example:
-```shell
-from charms.osm_libs.v0.utils import CharmError
-
-class MyCharm(CharmBase):
-    def _on_config_changed(self, _):
-        try:
-            if not self.config.get("some-option"):
-                raise CharmError("need some-option", BlockedStatus)
-
-            if not self.mysql_ready:
-                raise CharmError("waiting for mysql", WaitingStatus)
-
-            # Do stuff...
-
-        exception CharmError as e:
-            self.unit.status = e.status
-```
-
-# Pebble validations
-
-The `check_container_ready` function checks that a container is ready,
-and therefore Pebble is ready.
-
-The `check_service_active` function checks that a service in a container is running.
-
-Both functions raise a CharmError if the validations fail.
-
-Example:
-```shell
-from charms.osm_libs.v0.utils import check_container_ready, check_service_active
-
-class MyCharm(CharmBase):
-    def _on_config_changed(self, _):
-        try:
-            container: Container = self.unit.get_container("my-container")
-            check_container_ready(container)
-            check_service_active(container, "my-service")
-            # Do stuff...
-
-        exception CharmError as e:
-            self.unit.status = e.status
-```
-
-# Debug-mode
-
-The debug-mode allows OSM developers to easily debug OSM modules.
-
-Example:
-```shell
-from charms.osm_libs.v0.utils import DebugMode
-
-class MyCharm(CharmBase):
-    _stored = StoredState()
-
-    def __init__(self, _):
-        # ...
-        container: Container = self.unit.get_container("my-container")
-        hostpaths = [
-            HostPath(
-                config="module-hostpath",
-                container_path="/usr/lib/python3/dist-packages/module"
-            ),
-        ]
-        vscode_workspace_path = "files/vscode-workspace.json"
-        self.debug_mode = DebugMode(
-            self,
-            self._stored,
-            container,
-            hostpaths,
-            vscode_workspace_path,
-        )
-
-    def _on_update_status(self, _):
-        if self.debug_mode.started:
-            return
-        # ...
-
-    def _get_debug_mode_information(self):
-        command = self.debug_mode.command
-        password = self.debug_mode.password
-        return command, password
-```
-
-# More
-
-- Get pod IP with `get_pod_ip()`
-"""
-from dataclasses import dataclass
-import logging
-import secrets
-import socket
-from pathlib import Path
-from typing import List
-
-from lightkube import Client
-from lightkube.models.core_v1 import HostPathVolumeSource, Volume, VolumeMount
-from lightkube.resources.apps_v1 import StatefulSet
-from ops.charm import CharmBase
-from ops.framework import Object, StoredState
-from ops.model import (
-    ActiveStatus,
-    BlockedStatus,
-    Container,
-    MaintenanceStatus,
-    StatusBase,
-    WaitingStatus,
-)
-from ops.pebble import ServiceStatus
-
-# The unique Charmhub library identifier, never change it
-LIBID = "e915908eebee4cdd972d484728adf984"
-
-# 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 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)
-
-
-def check_container_ready(container: Container) -> None:
-    """Check Pebble has started in the container.
-
-    Args:
-        container (Container): Container to be checked.
-
-    Raises:
-        CharmError: if container is not ready.
-    """
-    if not container.can_connect():
-        raise CharmError("waiting for pebble to start", MaintenanceStatus)
-
-
-def check_service_active(container: Container, service_name: str) -> None:
-    """Check if the service is running.
-
-    Args:
-        container (Container): Container to be checked.
-        service_name (str): Name of the service to check.
-
-    Raises:
-        CharmError: if the service is not running.
-    """
-    if service_name not in container.get_plan().services:
-        raise CharmError(f"{service_name} service not configured yet", WaitingStatus)
-
-    if container.get_service(service_name).current != ServiceStatus.ACTIVE:
-        raise CharmError(f"{service_name} service is not running")
-
-
-def get_pod_ip() -> str:
-    """Get Kubernetes Pod IP.
-
-    Returns:
-        str: The IP of the Pod.
-    """
-    return socket.gethostbyname(socket.gethostname())
-
-
-_DEBUG_SCRIPT = r"""#!/bin/bash
-# Install SSH
-
-function download_code(){{
-    wget https://go.microsoft.com/fwlink/?LinkID=760868 -O code.deb
-}}
-
-function setup_envs(){{
-    grep "source /debug.envs" /root/.bashrc || echo "source /debug.envs" | tee -a /root/.bashrc
-}}
-function setup_ssh(){{
-    apt install ssh -y
-    cat /etc/ssh/sshd_config |
-        grep -E '^PermitRootLogin yes$$' || (
-        echo PermitRootLogin yes |
-        tee -a /etc/ssh/sshd_config
-    )
-    service ssh stop
-    sleep 3
-    service ssh start
-    usermod --password $(echo {} | openssl passwd -1 -stdin) root
-}}
-
-function setup_code(){{
-    apt install libasound2 -y
-    (dpkg -i code.deb || apt-get install -f -y || apt-get install -f -y) && echo Code installed successfully
-    code --install-extension ms-python.python --user-data-dir /root
-    mkdir -p /root/.vscode-server
-    cp -R /root/.vscode/extensions /root/.vscode-server/extensions
-}}
-
-export DEBIAN_FRONTEND=noninteractive
-apt update && apt install wget -y
-download_code &
-setup_ssh &
-setup_envs
-wait
-setup_code &
-wait
-"""
-
-
-@dataclass
-class SubModule:
-    """Represent RO Submodules."""
-    sub_module_path: str
-    container_path: str
-
-
-class HostPath:
-    """Represents a hostpath."""
-    def __init__(self, config: str, container_path: str, submodules: dict = None) -> None:
-        mount_path_items = config.split("-")
-        mount_path_items.reverse()
-        self.mount_path = "/" + "/".join(mount_path_items)
-        self.config = config
-        self.sub_module_dict = {}
-        if submodules:
-            for submodule in submodules.keys():
-                self.sub_module_dict[submodule] = SubModule(
-                    sub_module_path=self.mount_path + "/" + submodule,
-                    container_path=submodules[submodule],
-                )
-        else:
-            self.container_path = container_path
-            self.module_name = container_path.split("/")[-1]
-
-class DebugMode(Object):
-    """Class to handle the debug-mode."""
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        stored: StoredState,
-        container: Container,
-        hostpaths: List[HostPath] = [],
-        vscode_workspace_path: str = "files/vscode-workspace.json",
-    ) -> None:
-        super().__init__(charm, "debug-mode")
-
-        self.charm = charm
-        self._stored = stored
-        self.hostpaths = hostpaths
-        self.vscode_workspace = Path(vscode_workspace_path).read_text()
-        self.container = container
-
-        self._stored.set_default(
-            debug_mode_started=False,
-            debug_mode_vscode_command=None,
-            debug_mode_password=None,
-        )
-
-        self.framework.observe(self.charm.on.config_changed, self._on_config_changed)
-        self.framework.observe(self.charm.on[container.name].pebble_ready, self._on_config_changed)
-        self.framework.observe(self.charm.on.update_status, self._on_update_status)
-
-    def _on_config_changed(self, _) -> None:
-        """Handler for the config-changed event."""
-        if not self.charm.unit.is_leader():
-            return
-
-        debug_mode_enabled = self.charm.config.get("debug-mode", False)
-        action = self.enable if debug_mode_enabled else self.disable
-        action()
-
-    def _on_update_status(self, _) -> None:
-        """Handler for the update-status event."""
-        if not self.charm.unit.is_leader() or not self.started:
-            return
-
-        self.charm.unit.status = ActiveStatus("debug-mode: ready")
-
-    @property
-    def started(self) -> bool:
-        """Indicates whether the debug-mode has started or not."""
-        return self._stored.debug_mode_started
-
-    @property
-    def command(self) -> str:
-        """Command to launch vscode."""
-        return self._stored.debug_mode_vscode_command
-
-    @property
-    def password(self) -> str:
-        """SSH password."""
-        return self._stored.debug_mode_password
-
-    def enable(self, service_name: str = None) -> None:
-        """Enable debug-mode.
-
-        This function mounts hostpaths of the OSM modules (if set), and
-        configures the container so it can be easily debugged. The setup
-        includes the configuration of SSH, environment variables, and
-        VSCode workspace and plugins.
-
-        Args:
-            service_name (str, optional): Pebble service name which has the desired environment
-                variables. Mandatory if there is more than one Pebble service configured.
-        """
-        hostpaths_to_reconfigure = self._hostpaths_to_reconfigure()
-        if self.started and not hostpaths_to_reconfigure:
-            self.charm.unit.status = ActiveStatus("debug-mode: ready")
-            return
-
-        logger.debug("enabling debug-mode")
-
-        # Mount hostpaths if set.
-        # If hostpaths are mounted, the statefulset will be restarted,
-        # and for that reason we return immediately. On restart, the hostpaths
-        # won't be mounted and then we can continue and setup the debug-mode.
-        if hostpaths_to_reconfigure:
-            self.charm.unit.status = MaintenanceStatus("debug-mode: configuring hostpaths")
-            self._configure_hostpaths(hostpaths_to_reconfigure)
-            return
-
-        self.charm.unit.status = MaintenanceStatus("debug-mode: starting")
-        password = secrets.token_hex(8)
-        self._setup_debug_mode(
-            password,
-            service_name,
-            mounted_hostpaths=[hp for hp in self.hostpaths if self.charm.config.get(hp.config)],
-        )
-
-        self._stored.debug_mode_vscode_command = self._get_vscode_command(get_pod_ip())
-        self._stored.debug_mode_password = password
-        self._stored.debug_mode_started = True
-        logger.info("debug-mode is ready")
-        self.charm.unit.status = ActiveStatus("debug-mode: ready")
-
-    def disable(self) -> None:
-        """Disable debug-mode."""
-        logger.debug("disabling debug-mode")
-        current_status = self.charm.unit.status
-        hostpaths_unmounted = self._unmount_hostpaths()
-
-        if not self._stored.debug_mode_started:
-            return
-        self._stored.debug_mode_started = False
-        self._stored.debug_mode_vscode_command = None
-        self._stored.debug_mode_password = None
-
-        if not hostpaths_unmounted:
-            self.charm.unit.status = current_status
-            self._restart()
-
-    def _hostpaths_to_reconfigure(self) -> List[HostPath]:
-        hostpaths_to_reconfigure: List[HostPath] = []
-        client = Client()
-        statefulset = client.get(StatefulSet, self.charm.app.name, namespace=self.charm.model.name)
-        volumes = statefulset.spec.template.spec.volumes
-
-        for hostpath in self.hostpaths:
-            hostpath_is_set = True if self.charm.config.get(hostpath.config) else False
-            hostpath_already_configured = next(
-                (True for volume in volumes if volume.name == hostpath.config), False
-            )
-            if hostpath_is_set != hostpath_already_configured:
-                hostpaths_to_reconfigure.append(hostpath)
-
-        return hostpaths_to_reconfigure
-
-    def _setup_debug_mode(
-        self,
-        password: str,
-        service_name: str = None,
-        mounted_hostpaths: List[HostPath] = [],
-    ) -> None:
-        services = self.container.get_plan().services
-        if not service_name and len(services) != 1:
-            raise Exception("Cannot start debug-mode: please set the service_name")
-
-        service = None
-        if not service_name:
-            service_name, service = services.popitem()
-        if not service:
-            service = services.get(service_name)
-
-        logger.debug(f"getting environment variables from service {service_name}")
-        environment = service.environment
-        environment_file_content = "\n".join(
-            [f'export {key}="{value}"' for key, value in environment.items()]
-        )
-        logger.debug(f"pushing environment file to {self.container.name} container")
-        self.container.push("/debug.envs", environment_file_content)
-
-        # Push VSCode workspace
-        logger.debug(f"pushing vscode workspace to {self.container.name} container")
-        self.container.push("/debug.code-workspace", self.vscode_workspace)
-
-        # Execute debugging script
-        logger.debug(f"pushing debug-mode setup script to {self.container.name} container")
-        self.container.push("/debug.sh", _DEBUG_SCRIPT.format(password), permissions=0o777)
-        logger.debug(f"executing debug-mode setup script in {self.container.name} container")
-        self.container.exec(["/debug.sh"]).wait_output()
-        logger.debug(f"stopping service {service_name} in {self.container.name} container")
-        self.container.stop(service_name)
-
-        # Add symlinks to mounted hostpaths
-        for hostpath in mounted_hostpaths:
-            logger.debug(f"adding symlink for {hostpath.config}")
-            if len(hostpath.sub_module_dict) > 0:
-                for sub_module in hostpath.sub_module_dict.keys():
-                    self.container.exec(["rm", "-rf", hostpath.sub_module_dict[sub_module].container_path]).wait_output()
-                    self.container.exec(
-                        [
-                            "ln",
-                            "-s",
-                            hostpath.sub_module_dict[sub_module].sub_module_path,
-                            hostpath.sub_module_dict[sub_module].container_path,
-                        ]
-                    )
-
-            else:
-                self.container.exec(["rm", "-rf", hostpath.container_path]).wait_output()
-                self.container.exec(
-                    [
-                        "ln",
-                        "-s",
-                        f"{hostpath.mount_path}/{hostpath.module_name}",
-                        hostpath.container_path,
-                    ]
-                )
-
-    def _configure_hostpaths(self, hostpaths: List[HostPath]):
-        client = Client()
-        statefulset = client.get(StatefulSet, self.charm.app.name, namespace=self.charm.model.name)
-
-        for hostpath in hostpaths:
-            if self.charm.config.get(hostpath.config):
-                self._add_hostpath_to_statefulset(hostpath, statefulset)
-            else:
-                self._delete_hostpath_from_statefulset(hostpath, statefulset)
-
-        client.replace(statefulset)
-
-    def _unmount_hostpaths(self) -> bool:
-        client = Client()
-        hostpath_unmounted = False
-        statefulset = client.get(StatefulSet, self.charm.app.name, namespace=self.charm.model.name)
-
-        for hostpath in self.hostpaths:
-            if self._delete_hostpath_from_statefulset(hostpath, statefulset):
-                hostpath_unmounted = True
-
-        if hostpath_unmounted:
-            client.replace(statefulset)
-
-        return hostpath_unmounted
-
-    def _add_hostpath_to_statefulset(self, hostpath: HostPath, statefulset: StatefulSet):
-        # Add volume
-        logger.debug(f"adding volume {hostpath.config} to {self.charm.app.name} statefulset")
-        volume = Volume(
-            hostpath.config,
-            hostPath=HostPathVolumeSource(
-                path=self.charm.config[hostpath.config],
-                type="Directory",
-            ),
-        )
-        statefulset.spec.template.spec.volumes.append(volume)
-
-        # Add volumeMount
-        for statefulset_container in statefulset.spec.template.spec.containers:
-            if statefulset_container.name != self.container.name:
-                continue
-
-            logger.debug(
-                f"adding volumeMount {hostpath.config} to {self.container.name} container"
-            )
-            statefulset_container.volumeMounts.append(
-                VolumeMount(mountPath=hostpath.mount_path, name=hostpath.config)
-            )
-
-    def _delete_hostpath_from_statefulset(self, hostpath: HostPath, statefulset: StatefulSet):
-        hostpath_unmounted = False
-        for volume in statefulset.spec.template.spec.volumes:
-
-            if hostpath.config != volume.name:
-                continue
-
-            # Remove volumeMount
-            for statefulset_container in statefulset.spec.template.spec.containers:
-                if statefulset_container.name != self.container.name:
-                    continue
-                for volume_mount in statefulset_container.volumeMounts:
-                    if volume_mount.name != hostpath.config:
-                        continue
-
-                    logger.debug(
-                        f"removing volumeMount {hostpath.config} from {self.container.name} container"
-                    )
-                    statefulset_container.volumeMounts.remove(volume_mount)
-
-            # Remove volume
-            logger.debug(
-                f"removing volume {hostpath.config} from {self.charm.app.name} statefulset"
-            )
-            statefulset.spec.template.spec.volumes.remove(volume)
-
-            hostpath_unmounted = True
-        return hostpath_unmounted
-
-    def _get_vscode_command(
-        self,
-        pod_ip: str,
-        user: str = "root",
-        workspace_path: str = "/debug.code-workspace",
-    ) -> str:
-        return f"code --remote ssh-remote+{user}@{pod_ip} {workspace_path}"
-
-    def _restart(self):
-        self.container.exec(["kill", "-HUP", "1"])
diff --git a/installers/charm/juju-simplestreams-operator/metadata.yaml b/installers/charm/juju-simplestreams-operator/metadata.yaml
deleted file mode 100644 (file)
index 03b9aa6..0000000
+++ /dev/null
@@ -1,55 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Overview on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
-
-name: osm-juju-simplestreams
-
-# The following metadata are human-readable and will be published prominently on Charmhub.
-
-display-name: Juju simplestreams
-
-summary: Basic http server exposing simplestreams for juju
-
-description: |
-  TODO
-
-containers:
-  server:
-    resource: server-image
-
-# This file populates the Resources tab on Charmhub.
-
-resources:
-  server-image:
-    type: oci-image
-    description: OCI image for server
-    upstream-source: nginx:1.23.0
-
-peers:
-  peer:
-    interface: peer
-
-requires:
-  ingress:
-    interface: ingress
-    limit: 1
diff --git a/installers/charm/juju-simplestreams-operator/pyproject.toml b/installers/charm/juju-simplestreams-operator/pyproject.toml
deleted file mode 100644 (file)
index 16cf0f4..0000000
+++ /dev/null
@@ -1,52 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-
-# 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", "E501", "D107"]
-# D100, D101, D102, D103: Ignore missing docstrings in tests
-per-file-ignores = ["tests/*:D100,D101,D102,D103,D104"]
-docstring-convention = "google"
diff --git a/installers/charm/juju-simplestreams-operator/requirements.txt b/installers/charm/juju-simplestreams-operator/requirements.txt
deleted file mode 100644 (file)
index 398d4ad..0000000
+++ /dev/null
@@ -1,23 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-ops < 2.2
-lightkube
-lightkube-models
-# git+https://github.com/charmed-osm/config-validator/
diff --git a/installers/charm/juju-simplestreams-operator/src/charm.py b/installers/charm/juju-simplestreams-operator/src/charm.py
deleted file mode 100755 (executable)
index 555aab0..0000000
+++ /dev/null
@@ -1,249 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# Learn more at: https://juju.is/docs/sdk
-
-"""Juju simpletreams charm."""
-
-import logging
-import subprocess
-from dataclasses import dataclass
-from pathlib import Path
-from typing import Any, Dict
-
-from charms.nginx_ingress_integrator.v0.ingress import IngressRequires
-from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
-from charms.osm_libs.v0.utils import (
-    CharmError,
-    check_container_ready,
-    check_service_active,
-)
-from lightkube.models.core_v1 import ServicePort
-from ops.charm import ActionEvent, CharmBase
-from ops.main import main
-from ops.model import ActiveStatus, Container
-
-SERVICE_PORT = 8080
-
-logger = logging.getLogger(__name__)
-container_name = "server"
-
-
-@dataclass
-class ImageMetadata:
-    """Image Metadata."""
-
-    region: str
-    auth_url: str
-    image_id: str
-    series: str
-
-
-class JujuSimplestreamsCharm(CharmBase):
-    """Simplestreams Kubernetes sidecar charm."""
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.ingress = IngressRequires(
-            self,
-            {
-                "service-hostname": self.external_hostname,
-                "service-name": self.app.name,
-                "service-port": SERVICE_PORT,
-            },
-        )
-        event_handler_mapping = {
-            # Core lifecycle events
-            self.on["server"].pebble_ready: self._on_server_pebble_ready,
-            self.on.update_status: self._on_update_status,
-            self.on["peer"].relation_changed: self._push_image_metadata_from_relation,
-            # Action events
-            self.on["add-image-metadata"].action: self._on_add_image_metadata_action,
-        }
-
-        for event, handler in event_handler_mapping.items():
-            self.framework.observe(event, handler)
-
-        port = ServicePort(SERVICE_PORT, name=f"{self.app.name}")
-        self.service_patcher = KubernetesServicePatch(self, [port])
-        self.container: Container = self.unit.get_container(container_name)
-        self.unit.set_workload_version(self.unit.name)
-
-    @property
-    def external_hostname(self) -> str:
-        """External hostname property.
-
-        Returns:
-            str: the external hostname from config.
-                If not set, return the ClusterIP service name.
-        """
-        return self.config.get("external-hostname") or self.app.name
-
-    # ---------------------------------------------------------------------------
-    #   Handlers for Charm Events
-    # ---------------------------------------------------------------------------
-
-    def _on_server_pebble_ready(self, _) -> None:
-        """Handler for the config-changed event."""
-        try:
-            self._push_configuration()
-            self._configure_service()
-            self._push_image_metadata_from_relation()
-            # Update charm status
-            self._on_update_status()
-        except CharmError as e:
-            logger.debug(e.message)
-            self.unit.status = e.status
-
-    def _on_update_status(self, _=None) -> None:
-        """Handler for the update-status event."""
-        try:
-            check_container_ready(self.container)
-            check_service_active(self.container, container_name)
-            self.unit.status = ActiveStatus()
-        except CharmError as e:
-            logger.debug(e.message)
-            self.unit.status = e.status
-
-    def _push_image_metadata_from_relation(self, _=None):
-        subprocess.run(["rm", "-rf", "/tmp/simplestreams"])
-        subprocess.run(["mkdir", "-p", "/tmp/simplestreams"])
-        image_metadata_dict = self._get_image_metadata_from_relation()
-        for image_metadata in image_metadata_dict.values():
-            subprocess.run(
-                [
-                    "files/juju-metadata",
-                    "generate-image",
-                    "-d",
-                    "/tmp/simplestreams",
-                    "-i",
-                    image_metadata.image_id,
-                    "-s",
-                    image_metadata.series,
-                    "-r",
-                    image_metadata.region,
-                    "-u",
-                    image_metadata.auth_url,
-                ]
-            )
-        subprocess.run(["chmod", "555", "-R", "/tmp/simplestreams"])
-        self.container.push_path("/tmp/simplestreams", "/app/static")
-
-    def _on_add_image_metadata_action(self, event: ActionEvent):
-        relation = self.model.get_relation("peer")
-        try:
-            if not relation:
-                raise Exception("charm has not been fully initialized. Try again later.")
-            if not self.unit.is_leader():
-                raise Exception("I am not the leader!")
-            if any(
-                prohibited_char in param_value
-                for prohibited_char in ",; "
-                for param_value in event.params.values()
-            ):
-                event.fail("invalid params")
-                return
-
-            image_metadata_dict = self._get_image_metadata_from_relation()
-
-            new_image_metadata = ImageMetadata(
-                region=event.params["region"],
-                auth_url=event.params["auth-url"],
-                image_id=event.params["image-id"],
-                series=event.params["series"],
-            )
-
-            image_metadata_dict[event.params["image-id"]] = new_image_metadata
-
-            new_relation_data = []
-            for image_metadata in image_metadata_dict.values():
-                new_relation_data.append(
-                    f"{image_metadata.image_id};{image_metadata.series};{image_metadata.region};{image_metadata.auth_url}"
-                )
-            relation.data[self.app]["data"] = ",".join(new_relation_data)
-        except Exception as e:
-            event.fail(f"Action failed: {e}")
-            logger.error(f"Action failed: {e}")
-
-    # ---------------------------------------------------------------------------
-    #   Validation and configuration and more
-    # ---------------------------------------------------------------------------
-
-    def _get_image_metadata_from_relation(self) -> Dict[str, ImageMetadata]:
-        if not (relation := self.model.get_relation("peer")):
-            return {}
-
-        image_metadata_dict: Dict[str, ImageMetadata] = {}
-
-        relation_data = relation.data[self.app].get("data", "")
-        if relation_data:
-            for image_metadata_string in relation_data.split(","):
-                image_id, series, region, auth_url = image_metadata_string.split(";")
-                image_metadata_dict[image_id] = ImageMetadata(
-                    region=region,
-                    auth_url=auth_url,
-                    image_id=image_id,
-                    series=series,
-                )
-
-        return image_metadata_dict
-
-    def _configure_service(self) -> None:
-        """Add Pebble layer with the ro service."""
-        logger.debug(f"configuring {self.app.name} service")
-        self.container.add_layer(container_name, self._get_layer(), combine=True)
-        self.container.replan()
-
-    def _push_configuration(self) -> None:
-        """Push nginx configuration to the container."""
-        self.container.push("/etc/nginx/nginx.conf", Path("files/nginx.conf").read_text())
-        self.container.make_dir("/app/static", make_parents=True)
-
-    def _update_ingress_config(self) -> None:
-        """Update ingress config in relation."""
-        ingress_config = {
-            "service-hostname": self.external_hostname,
-            "max-body-size": self.config["max-body-size"],
-        }
-        if "tls-secret-name" in self.config:
-            ingress_config["tls-secret-name"] = self.config["tls-secret-name"]
-        logger.debug(f"updating ingress-config: {ingress_config}")
-        self.ingress.update_config(ingress_config)
-
-    def _get_layer(self) -> Dict[str, Any]:
-        """Get layer for Pebble."""
-        return {
-            "summary": "server layer",
-            "description": "pebble config layer for server",
-            "services": {
-                container_name: {
-                    "override": "replace",
-                    "summary": "server service",
-                    "command": 'nginx -g "daemon off;"',
-                    "startup": "enabled",
-                }
-            },
-        }
-
-
-if __name__ == "__main__":  # pragma: no cover
-    main(JujuSimplestreamsCharm)
diff --git a/installers/charm/juju-simplestreams-operator/tests/unit/test_charm.py b/installers/charm/juju-simplestreams-operator/tests/unit/test_charm.py
deleted file mode 100644 (file)
index 0273352..0000000
+++ /dev/null
@@ -1,68 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-# Learn more about testing at: https://juju.is/docs/sdk/testing
-
-import pytest
-from ops.model import ActiveStatus
-from ops.testing import Harness
-from pytest_mock import MockerFixture
-
-from charm import JujuSimplestreamsCharm
-
-container_name = "server"
-service_name = "server"
-
-
-@pytest.fixture
-def harness(mocker: MockerFixture):
-    mocker.patch("charm.KubernetesServicePatch", lambda x, y: None)
-    harness = Harness(JujuSimplestreamsCharm)
-    harness.begin()
-    harness.charm.container.make_dir("/etc/nginx", make_parents=True)
-    yield harness
-    harness.cleanup()
-
-
-def test_ready(harness: Harness):
-    harness.charm.on.server_pebble_ready.emit(container_name)
-    assert harness.charm.unit.status == ActiveStatus()
-
-
-def test_add_metadata_action(harness: Harness, mocker: MockerFixture):
-    harness.set_leader(True)
-    remote_unit = f"{harness.charm.app.name}/1"
-    relation_id = harness.add_relation("peer", harness.charm.app.name)
-    harness.add_relation_unit(relation_id, remote_unit)
-    event = mocker.Mock()
-    event.params = {
-        "region": "microstack",
-        "auth-url": "localhost",
-        "image-id": "id",
-        "series": "focal",
-    }
-    harness.charm._on_add_image_metadata_action(event)
-    # Harness not emitting relation changed event when in the action
-    # I update application data in the peer relation.
-    # Manually emitting it here:
-    relation = harness.charm.model.get_relation("peer")
-    harness.charm.on["peer"].relation_changed.emit(relation)
-    assert harness.charm.container.exists("/app/static/simplestreams/images/streams/v1/index.json")
diff --git a/installers/charm/juju-simplestreams-operator/tox.ini b/installers/charm/juju-simplestreams-operator/tox.ini
deleted file mode 100644 (file)
index 0268da8..0000000
+++ /dev/null
@@ -1,91 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-
-[tox]
-skipsdist=True
-skip_missing_interpreters = True
-envlist = lint, unit
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-all_path = {[vars]src_path} {[vars]tst_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-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    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
-    # 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} \
-        -m pytest --ignore={[vars]tst_path}integration -v --tb native -s {posargs}
-    coverage report
-    coverage xml
-
-[testenv:integration]
-description = Run integration tests
-deps =
-    pytest
-    juju
-    pytest-operator
-    -r{toxinidir}/requirements.txt
-commands =
-    pytest -v --tb native --ignore={[vars]tst_path}unit --log-cli-level=INFO -s {posargs}
diff --git a/installers/charm/kafka-exporter/.gitignore b/installers/charm/kafka-exporter/.gitignore
deleted file mode 100644 (file)
index 2885df2..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-venv
-.vscode
-build
-*.charm
-.coverage
-coverage.xml
-.stestr
-cover
-release
\ No newline at end of file
diff --git a/installers/charm/kafka-exporter/.jujuignore b/installers/charm/kafka-exporter/.jujuignore
deleted file mode 100644 (file)
index 3ae3e7d..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-venv
-.vscode
-build
-*.charm
-.coverage
-coverage.xml
-.gitignore
-.stestr
-cover
-release
-tests/
-requirements*
-tox.ini
diff --git a/installers/charm/kafka-exporter/.yamllint.yaml b/installers/charm/kafka-exporter/.yamllint.yaml
deleted file mode 100644 (file)
index d71fb69..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
----
-extends: default
-
-yaml-files:
-  - "*.yaml"
-  - "*.yml"
-  - ".yamllint"
-ignore: |
-  .tox
-  cover/
-  build/
-  venv
-  release/
diff --git a/installers/charm/kafka-exporter/README.md b/installers/charm/kafka-exporter/README.md
deleted file mode 100644 (file)
index ae9babf..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<!-- 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.
-
-For those usages not covered by the Apache License, Version 2.0 please
-contact: legal@canonical.com
-
-To get in touch with the maintainers, please contact:
-osm-charmers@lists.launchpad.net -->
-
-# Prometheus kafka exporter operator Charm for Kubernetes
-
-## Requirements
diff --git a/installers/charm/kafka-exporter/charmcraft.yaml b/installers/charm/kafka-exporter/charmcraft.yaml
deleted file mode 100644 (file)
index 0a285a9..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-type: charm
-bases:
-  - build-on:
-      - name: ubuntu
-        channel: "20.04"
-        architectures: ["amd64"]
-    run-on:
-      - name: ubuntu
-        channel: "20.04"
-        architectures:
-          - amd64
-          - aarch64
-          - arm64
-parts:
-  charm:
-    build-packages: [git]
diff --git a/installers/charm/kafka-exporter/config.yaml b/installers/charm/kafka-exporter/config.yaml
deleted file mode 100644 (file)
index 5931336..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-options:
-  ingress_class:
-    type: string
-    description: |
-      Ingress class name. This is useful for selecting the ingress to be used
-      in case there are multiple ingresses in the underlying k8s clusters.
-  ingress_whitelist_source_range:
-    type: string
-    description: |
-      A comma-separated list of CIDRs to store in the
-      ingress.kubernetes.io/whitelist-source-range annotation.
-
-      This can be used to lock down access to
-      Keystone based on source IP address.
-    default: ""
-  tls_secret_name:
-    type: string
-    description: TLS Secret name
-    default: ""
-  site_url:
-    type: string
-    description: Ingress URL
-    default: ""
-  cluster_issuer:
-    type: string
-    description: Name of the cluster issuer for TLS certificates
-    default: ""
-  image_pull_policy:
-    type: string
-    description: |
-      ImagePullPolicy configuration for the pod.
-      Possible values: always, ifnotpresent, never
-    default: always
-  security_context:
-    description: Enables the security context of the pods
-    type: boolean
-    default: false
-  kafka_endpoint:
-    description: Host and port of Kafka in the format <host>:<port>
-    type: string
diff --git a/installers/charm/kafka-exporter/lib/charms/kafka_k8s/v0/kafka.py b/installers/charm/kafka-exporter/lib/charms/kafka_k8s/v0/kafka.py
deleted file mode 100644 (file)
index 1baf9a8..0000000
+++ /dev/null
@@ -1,207 +0,0 @@
-# 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.
-
-"""Kafka library.
-
-This [library](https://juju.is/docs/sdk/libraries) implements both sides of the
-`kafka` [interface](https://juju.is/docs/sdk/relations).
-
-The *provider* side of this interface is implemented by the
-[kafka-k8s Charmed Operator](https://charmhub.io/kafka-k8s).
-
-Any Charmed Operator that *requires* Kafka for providing its
-service should implement the *requirer* side of this interface.
-
-In a nutshell using this library to implement a Charmed Operator *requiring*
-Kafka would look like
-
-```
-$ charmcraft fetch-lib charms.kafka_k8s.v0.kafka
-```
-
-`metadata.yaml`:
-
-```
-requires:
-  kafka:
-    interface: kafka
-    limit: 1
-```
-
-`src/charm.py`:
-
-```
-from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
-from ops.charm import CharmBase
-
-
-class MyCharm(CharmBase):
-
-    on = KafkaEvents()
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.kafka = KafkaRequires(self)
-        self.framework.observe(
-            self.on.kafka_available,
-            self._on_kafka_available,
-        )
-        self.framework.observe(
-            self.on.kafka_broken,
-            self._on_kafka_broken,
-        )
-
-    def _on_kafka_available(self, event):
-        # Get Kafka host and port
-        host: str = self.kafka.host
-        port: int = self.kafka.port
-        # host => "kafka-k8s"
-        # port => 9092
-
-    def _on_kafka_broken(self, event):
-        # Stop service
-        # ...
-        self.unit.status = BlockedStatus("need kafka relation")
-```
-
-You can file bugs
-[here](https://github.com/charmed-osm/kafka-k8s-operator/issues)!
-"""
-
-from typing import Optional
-
-from ops.charm import CharmBase, CharmEvents
-from ops.framework import EventBase, EventSource, Object
-
-# The unique Charmhub library identifier, never change it
-from ops.model import Relation
-
-LIBID = "eacc8c85082347c9aae740e0220b8376"
-
-# 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
-
-
-KAFKA_HOST_APP_KEY = "host"
-KAFKA_PORT_APP_KEY = "port"
-
-
-class _KafkaAvailableEvent(EventBase):
-    """Event emitted when Kafka is available."""
-
-
-class _KafkaBrokenEvent(EventBase):
-    """Event emitted when Kafka relation is broken."""
-
-
-class KafkaEvents(CharmEvents):
-    """Kafka events.
-
-    This class defines the events that Kafka can emit.
-
-    Events:
-        kafka_available (_KafkaAvailableEvent)
-    """
-
-    kafka_available = EventSource(_KafkaAvailableEvent)
-    kafka_broken = EventSource(_KafkaBrokenEvent)
-
-
-class KafkaRequires(Object):
-    """Requires-side of the Kafka relation."""
-
-    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
-        super().__init__(charm, endpoint_name)
-        self.charm = charm
-        self._endpoint_name = endpoint_name
-
-        # Observe relation events
-        event_observe_mapping = {
-            charm.on[self._endpoint_name].relation_changed: self._on_relation_changed,
-            charm.on[self._endpoint_name].relation_broken: self._on_relation_broken,
-        }
-        for event, observer in event_observe_mapping.items():
-            self.framework.observe(event, observer)
-
-    def _on_relation_changed(self, event) -> None:
-        if event.relation.app and all(
-            key in event.relation.data[event.relation.app]
-            for key in (KAFKA_HOST_APP_KEY, KAFKA_PORT_APP_KEY)
-        ):
-            self.charm.on.kafka_available.emit()
-
-    def _on_relation_broken(self, _) -> None:
-        self.charm.on.kafka_broken.emit()
-
-    @property
-    def host(self) -> str:
-        relation: Relation = self.model.get_relation(self._endpoint_name)
-        return (
-            relation.data[relation.app].get(KAFKA_HOST_APP_KEY)
-            if relation and relation.app
-            else None
-        )
-
-    @property
-    def port(self) -> int:
-        relation: Relation = self.model.get_relation(self._endpoint_name)
-        return (
-            int(relation.data[relation.app].get(KAFKA_PORT_APP_KEY))
-            if relation and relation.app
-            else None
-        )
-
-
-class KafkaProvides(Object):
-    """Provides-side of the Kafka relation."""
-
-    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
-        super().__init__(charm, endpoint_name)
-        self._endpoint_name = endpoint_name
-
-    def set_host_info(self, host: str, port: int, relation: Optional[Relation] = None) -> None:
-        """Set Kafka host and port.
-
-        This function writes in the application data of the relation, therefore,
-        only the unit leader can call it.
-
-        Args:
-            host (str): Kafka hostname or IP address.
-            port (int): Kafka port.
-            relation (Optional[Relation]): Relation to update.
-                                           If not specified, all relations will be updated.
-
-        Raises:
-            Exception: if a non-leader unit calls this function.
-        """
-        if not self.model.unit.is_leader():
-            raise Exception("only the leader set host information.")
-
-        if relation:
-            self._update_relation_data(host, port, relation)
-            return
-
-        for relation in self.model.relations[self._endpoint_name]:
-            self._update_relation_data(host, port, relation)
-
-    def _update_relation_data(self, host: str, port: int, relation: Relation) -> None:
-        """Update data in relation if needed."""
-        relation.data[self.model.app][KAFKA_HOST_APP_KEY] = host
-        relation.data[self.model.app][KAFKA_PORT_APP_KEY] = str(port)
diff --git a/installers/charm/kafka-exporter/metadata.yaml b/installers/charm/kafka-exporter/metadata.yaml
deleted file mode 100644 (file)
index a70b3b6..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-name: kafka-exporter-k8s
-summary: OSM Prometheus Kafka Exporter
-description: |
-  A CAAS charm to deploy OSM's Prometheus Kafka Exporter.
-series:
-  - kubernetes
-tags:
-  - kubernetes
-  - osm
-  - prometheus
-  - kafka-exporter
-min-juju-version: 2.8.0
-deployment:
-  type: stateless
-  service: cluster
-resources:
-  image:
-    type: oci-image
-    description: Image of kafka-exporter
-    upstream-source: "bitnami/kafka-exporter:1.4.2"
-requires:
-  kafka:
-    interface: kafka
-provides:
-  prometheus-scrape:
-    interface: prometheus
-  grafana-dashboard:
-    interface: grafana-dashboard
diff --git a/installers/charm/kafka-exporter/requirements-test.txt b/installers/charm/kafka-exporter/requirements-test.txt
deleted file mode 100644 (file)
index 316f6d2..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-
-mock==4.0.3
diff --git a/installers/charm/kafka-exporter/requirements.txt b/installers/charm/kafka-exporter/requirements.txt
deleted file mode 100644 (file)
index 8bb93ad..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-git+https://github.com/charmed-osm/ops-lib-charmed-osm/@master
diff --git a/installers/charm/kafka-exporter/src/charm.py b/installers/charm/kafka-exporter/src/charm.py
deleted file mode 100755 (executable)
index 07a854f..0000000
+++ /dev/null
@@ -1,266 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-# pylint: disable=E0213
-
-from ipaddress import ip_network
-import logging
-from pathlib import Path
-from typing import NoReturn, Optional
-from urllib.parse import urlparse
-
-from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
-from ops.main import main
-from opslib.osm.charm import CharmedOsmBase, RelationsMissing
-from opslib.osm.interfaces.grafana import GrafanaDashboardTarget
-from opslib.osm.interfaces.prometheus import PrometheusScrapeTarget
-from opslib.osm.pod import (
-    ContainerV3Builder,
-    IngressResourceV3Builder,
-    PodSpecV3Builder,
-)
-from opslib.osm.validator import ModelValidator, validator
-
-
-logger = logging.getLogger(__name__)
-
-PORT = 9308
-
-
-class ConfigModel(ModelValidator):
-    site_url: Optional[str]
-    cluster_issuer: Optional[str]
-    ingress_class: Optional[str]
-    ingress_whitelist_source_range: Optional[str]
-    tls_secret_name: Optional[str]
-    image_pull_policy: str
-    security_context: bool
-    kafka_endpoint: Optional[str]
-
-    @validator("site_url")
-    def validate_site_url(cls, v):
-        if v:
-            parsed = urlparse(v)
-            if not parsed.scheme.startswith("http"):
-                raise ValueError("value must start with http")
-        return v
-
-    @validator("ingress_whitelist_source_range")
-    def validate_ingress_whitelist_source_range(cls, v):
-        if v:
-            ip_network(v)
-        return v
-
-    @validator("image_pull_policy")
-    def validate_image_pull_policy(cls, v):
-        values = {
-            "always": "Always",
-            "ifnotpresent": "IfNotPresent",
-            "never": "Never",
-        }
-        v = v.lower()
-        if v not in values.keys():
-            raise ValueError("value must be always, ifnotpresent or never")
-        return values[v]
-
-    @validator("kafka_endpoint")
-    def validate_kafka_endpoint(cls, v):
-        if v and len(v.split(":")) != 2:
-            raise ValueError("value must be in the format <host>:<port>")
-        return v
-
-
-class KafkaEndpoint:
-    def __init__(self, host: str, port: str) -> None:
-        self.host = host
-        self.port = port
-
-
-class KafkaExporterCharm(CharmedOsmBase):
-    on = KafkaEvents()
-
-    def __init__(self, *args) -> NoReturn:
-        super().__init__(*args, oci_image="image")
-
-        # Provision Kafka relation to exchange information
-        self.kafka = KafkaRequires(self)
-        self.framework.observe(self.on.kafka_available, self.configure_pod)
-        self.framework.observe(self.on.kafka_broken, self.configure_pod)
-
-        # Register relation to provide a Scraping Target
-        self.scrape_target = PrometheusScrapeTarget(self, "prometheus-scrape")
-        self.framework.observe(
-            self.on["prometheus-scrape"].relation_joined, self._publish_scrape_info
-        )
-
-        # Register relation to provide a Dasboard Target
-        self.dashboard_target = GrafanaDashboardTarget(self, "grafana-dashboard")
-        self.framework.observe(
-            self.on["grafana-dashboard"].relation_joined, self._publish_dashboard_info
-        )
-
-    def _publish_scrape_info(self, event) -> NoReturn:
-        """Publishes scraping information for Prometheus.
-
-        Args:
-            event (EventBase): Prometheus relation event.
-        """
-        if self.unit.is_leader():
-            hostname = (
-                urlparse(self.model.config["site_url"]).hostname
-                if self.model.config["site_url"]
-                else self.model.app.name
-            )
-            port = str(PORT)
-            if self.model.config.get("site_url", "").startswith("https://"):
-                port = "443"
-            elif self.model.config.get("site_url", "").startswith("http://"):
-                port = "80"
-
-            self.scrape_target.publish_info(
-                hostname=hostname,
-                port=port,
-                metrics_path="/metrics",
-                scrape_interval="30s",
-                scrape_timeout="15s",
-            )
-
-    def _publish_dashboard_info(self, event) -> NoReturn:
-        """Publish dashboards for Grafana.
-
-        Args:
-            event (EventBase): Grafana relation event.
-        """
-        if self.unit.is_leader():
-            self.dashboard_target.publish_info(
-                name="osm-kafka",
-                dashboard=Path("templates/kafka_exporter_dashboard.json").read_text(),
-            )
-
-    def _is_kafka_endpoint_set(self, config: ConfigModel) -> bool:
-        """Check if Kafka endpoint is set."""
-        return config.kafka_endpoint or self._is_kafka_relation_set()
-
-    def _is_kafka_relation_set(self) -> bool:
-        """Check if the Kafka relation is set or not."""
-        return self.kafka.host and self.kafka.port
-
-    @property
-    def kafka_endpoint(self) -> KafkaEndpoint:
-        config = ConfigModel(**dict(self.config))
-        if config.kafka_endpoint:
-            host, port = config.kafka_endpoint.split(":")
-        else:
-            host = self.kafka.host
-            port = self.kafka.port
-        return KafkaEndpoint(host, port)
-
-    def build_pod_spec(self, image_info):
-        """Build the PodSpec to be used.
-
-        Args:
-            image_info (str): container image information.
-
-        Returns:
-            Dict: PodSpec information.
-        """
-        # Validate config
-        config = ConfigModel(**dict(self.config))
-
-        # Check relations
-        if not self._is_kafka_endpoint_set(config):
-            raise RelationsMissing(["kafka"])
-
-        # Create Builder for the PodSpec
-        pod_spec_builder = PodSpecV3Builder(
-            enable_security_context=config.security_context
-        )
-
-        # Build container
-        container_builder = ContainerV3Builder(
-            self.app.name,
-            image_info,
-            config.image_pull_policy,
-            run_as_non_root=config.security_context,
-        )
-        container_builder.add_port(name="exporter", port=PORT)
-        container_builder.add_http_readiness_probe(
-            path="/api/health",
-            port=PORT,
-            initial_delay_seconds=10,
-            period_seconds=10,
-            timeout_seconds=5,
-            success_threshold=1,
-            failure_threshold=3,
-        )
-        container_builder.add_http_liveness_probe(
-            path="/api/health",
-            port=PORT,
-            initial_delay_seconds=60,
-            timeout_seconds=30,
-            failure_threshold=10,
-        )
-        container_builder.add_command(
-            [
-                "kafka_exporter",
-                f"--kafka.server={self.kafka_endpoint.host}:{self.kafka_endpoint.port}",
-            ]
-        )
-        container = container_builder.build()
-
-        # Add container to PodSpec
-        pod_spec_builder.add_container(container)
-
-        # Add ingress resources to PodSpec if site url exists
-        if config.site_url:
-            parsed = urlparse(config.site_url)
-            annotations = {}
-            if config.ingress_class:
-                annotations["kubernetes.io/ingress.class"] = config.ingress_class
-            ingress_resource_builder = IngressResourceV3Builder(
-                f"{self.app.name}-ingress", annotations
-            )
-
-            if config.ingress_whitelist_source_range:
-                annotations[
-                    "nginx.ingress.kubernetes.io/whitelist-source-range"
-                ] = config.ingress_whitelist_source_range
-
-            if config.cluster_issuer:
-                annotations["cert-manager.io/cluster-issuer"] = config.cluster_issuer
-
-            if parsed.scheme == "https":
-                ingress_resource_builder.add_tls(
-                    [parsed.hostname], config.tls_secret_name
-                )
-            else:
-                annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
-
-            ingress_resource_builder.add_rule(parsed.hostname, self.app.name, PORT)
-            ingress_resource = ingress_resource_builder.build()
-            pod_spec_builder.add_ingress_resource(ingress_resource)
-
-        return pod_spec_builder.build()
-
-
-if __name__ == "__main__":
-    main(KafkaExporterCharm)
diff --git a/installers/charm/kafka-exporter/src/pod_spec.py b/installers/charm/kafka-exporter/src/pod_spec.py
deleted file mode 100644 (file)
index 214d652..0000000
+++ /dev/null
@@ -1,314 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-from ipaddress import ip_network
-import logging
-from typing import Any, Dict, List
-from urllib.parse import urlparse
-
-logger = logging.getLogger(__name__)
-
-
-def _validate_ip_network(network: str) -> bool:
-    """Validate IP network.
-
-    Args:
-        network (str): IP network range.
-
-    Returns:
-        bool: True if valid, false otherwise.
-    """
-    if not network:
-        return True
-
-    try:
-        ip_network(network)
-    except ValueError:
-        return False
-
-    return True
-
-
-def _validate_data(config_data: Dict[str, Any], relation_data: Dict[str, Any]) -> bool:
-    """Validates passed information.
-
-    Args:
-        config_data (Dict[str, Any]): configuration information.
-        relation_data (Dict[str, Any]): relation information
-
-    Raises:
-        ValueError: when config and/or relation data is not valid.
-    """
-    config_validators = {
-        "site_url": lambda value, _: isinstance(value, str)
-        if value is not None
-        else True,
-        "cluster_issuer": lambda value, _: isinstance(value, str)
-        if value is not None
-        else True,
-        "ingress_whitelist_source_range": lambda value, _: _validate_ip_network(value),
-        "tls_secret_name": lambda value, _: isinstance(value, str)
-        if value is not None
-        else True,
-    }
-    relation_validators = {
-        "kafka_host": lambda value, _: isinstance(value, str) and len(value) > 0,
-        "kafka_port": lambda value, _: isinstance(value, str)
-        and len(value) > 0
-        and int(value) > 0,
-    }
-    problems = []
-
-    for key, validator in config_validators.items():
-        valid = validator(config_data.get(key), config_data)
-
-        if not valid:
-            problems.append(key)
-
-    for key, validator in relation_validators.items():
-        valid = validator(relation_data.get(key), relation_data)
-
-        if not valid:
-            problems.append(key)
-
-    if len(problems) > 0:
-        raise ValueError("Errors found in: {}".format(", ".join(problems)))
-
-    return True
-
-
-def _make_pod_ports(port: int) -> List[Dict[str, Any]]:
-    """Generate pod ports details.
-
-    Args:
-        port (int): port to expose.
-
-    Returns:
-        List[Dict[str, Any]]: pod port details.
-    """
-    return [{"name": "kafka-exporter", "containerPort": port, "protocol": "TCP"}]
-
-
-def _make_pod_envconfig(
-    config: Dict[str, Any], relation_state: Dict[str, Any]
-) -> Dict[str, Any]:
-    """Generate pod environment configuration.
-
-    Args:
-        config (Dict[str, Any]): configuration information.
-        relation_state (Dict[str, Any]): relation state information.
-
-    Returns:
-        Dict[str, Any]: pod environment configuration.
-    """
-    envconfig = {}
-
-    return envconfig
-
-
-def _make_pod_ingress_resources(
-    config: Dict[str, Any], app_name: str, port: int
-) -> List[Dict[str, Any]]:
-    """Generate pod ingress resources.
-
-    Args:
-        config (Dict[str, Any]): configuration information.
-        app_name (str): application name.
-        port (int): port to expose.
-
-    Returns:
-        List[Dict[str, Any]]: pod ingress resources.
-    """
-    site_url = config.get("site_url")
-
-    if not site_url:
-        return
-
-    parsed = urlparse(site_url)
-
-    if not parsed.scheme.startswith("http"):
-        return
-
-    ingress_whitelist_source_range = config["ingress_whitelist_source_range"]
-    cluster_issuer = config["cluster_issuer"]
-
-    annotations = {}
-
-    if ingress_whitelist_source_range:
-        annotations[
-            "nginx.ingress.kubernetes.io/whitelist-source-range"
-        ] = ingress_whitelist_source_range
-
-    if cluster_issuer:
-        annotations["cert-manager.io/cluster-issuer"] = cluster_issuer
-
-    ingress_spec_tls = None
-
-    if parsed.scheme == "https":
-        ingress_spec_tls = [{"hosts": [parsed.hostname]}]
-        tls_secret_name = config["tls_secret_name"]
-        if tls_secret_name:
-            ingress_spec_tls[0]["secretName"] = tls_secret_name
-    else:
-        annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
-
-    ingress = {
-        "name": "{}-ingress".format(app_name),
-        "annotations": annotations,
-        "spec": {
-            "rules": [
-                {
-                    "host": parsed.hostname,
-                    "http": {
-                        "paths": [
-                            {
-                                "path": "/",
-                                "backend": {
-                                    "serviceName": app_name,
-                                    "servicePort": port,
-                                },
-                            }
-                        ]
-                    },
-                }
-            ]
-        },
-    }
-    if ingress_spec_tls:
-        ingress["spec"]["tls"] = ingress_spec_tls
-
-    return [ingress]
-
-
-def _make_readiness_probe(port: int) -> Dict[str, Any]:
-    """Generate readiness probe.
-
-    Args:
-        port (int): service port.
-
-    Returns:
-        Dict[str, Any]: readiness probe.
-    """
-    return {
-        "httpGet": {
-            "path": "/api/health",
-            "port": port,
-        },
-        "initialDelaySeconds": 10,
-        "periodSeconds": 10,
-        "timeoutSeconds": 5,
-        "successThreshold": 1,
-        "failureThreshold": 3,
-    }
-
-
-def _make_liveness_probe(port: int) -> Dict[str, Any]:
-    """Generate liveness probe.
-
-    Args:
-        port (int): service port.
-
-    Returns:
-        Dict[str, Any]: liveness probe.
-    """
-    return {
-        "httpGet": {
-            "path": "/api/health",
-            "port": port,
-        },
-        "initialDelaySeconds": 60,
-        "timeoutSeconds": 30,
-        "failureThreshold": 10,
-    }
-
-
-def _make_pod_command(relation: Dict[str, Any]) -> List[str]:
-    """Generate the startup command.
-
-    Args:
-        relation (Dict[str, Any]): Relation information.
-
-    Returns:
-        List[str]: command to startup the process.
-    """
-    command = [
-        "kafka_exporter",
-        "--kafka.server={}:{}".format(
-            relation.get("kafka_host"), relation.get("kafka_port")
-        ),
-    ]
-
-    return command
-
-
-def make_pod_spec(
-    image_info: Dict[str, str],
-    config: Dict[str, Any],
-    relation_state: Dict[str, Any],
-    app_name: str = "kafka-exporter",
-    port: int = 9308,
-) -> Dict[str, Any]:
-    """Generate the pod spec information.
-
-    Args:
-        image_info (Dict[str, str]): Object provided by
-                                     OCIImageResource("image").fetch().
-        config (Dict[str, Any]): Configuration information.
-        relation_state (Dict[str, Any]): Relation state information.
-        app_name (str, optional): Application name. Defaults to "ro".
-        port (int, optional): Port for the container. Defaults to 9090.
-
-    Returns:
-        Dict[str, Any]: Pod spec dictionary for the charm.
-    """
-    if not image_info:
-        return None
-
-    _validate_data(config, relation_state)
-
-    ports = _make_pod_ports(port)
-    env_config = _make_pod_envconfig(config, relation_state)
-    readiness_probe = _make_readiness_probe(port)
-    liveness_probe = _make_liveness_probe(port)
-    ingress_resources = _make_pod_ingress_resources(config, app_name, port)
-    command = _make_pod_command(relation_state)
-
-    return {
-        "version": 3,
-        "containers": [
-            {
-                "name": app_name,
-                "imageDetails": image_info,
-                "imagePullPolicy": "Always",
-                "ports": ports,
-                "envConfig": env_config,
-                "command": command,
-                "kubernetes": {
-                    "readinessProbe": readiness_probe,
-                    "livenessProbe": liveness_probe,
-                },
-            }
-        ],
-        "kubernetesResources": {
-            "ingressResources": ingress_resources or [],
-        },
-    }
diff --git a/installers/charm/kafka-exporter/templates/kafka_exporter_dashboard.json b/installers/charm/kafka-exporter/templates/kafka_exporter_dashboard.json
deleted file mode 100644 (file)
index 5b7552a..0000000
+++ /dev/null
@@ -1,609 +0,0 @@
-{
-  "annotations": {
-    "list": [
-      {
-        "builtIn": 1,
-        "datasource": "-- Grafana --",
-        "enable": true,
-        "hide": true,
-        "iconColor": "rgba(0, 211, 255, 1)",
-        "name": "Annotations & Alerts",
-        "type": "dashboard"
-      }
-    ]
-  },
-  "description": "Kafka resource usage and throughput",
-  "editable": true,
-  "gnetId": 7589,
-  "graphTooltip": 0,
-  "id": 10,
-  "iteration": 1578848023483,
-  "links": [],
-  "panels": [
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fill": 0,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 10,
-        "w": 10,
-        "x": 0,
-        "y": 0
-      },
-      "id": 14,
-      "legend": {
-        "alignAsTable": true,
-        "avg": false,
-        "current": true,
-        "max": true,
-        "min": false,
-        "rightSide": false,
-        "show": true,
-        "sideWidth": 480,
-        "sort": "max",
-        "sortDesc": true,
-        "total": false,
-        "values": true
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "connected",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "sum(kafka_topic_partition_current_offset - kafka_topic_partition_oldest_offset{instance=\"$instance\", topic=~\"$topic\"}) by (topic)",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "{{topic}}",
-          "refId": "B"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Messages stored per topic",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": "0",
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fill": 0,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 10,
-        "w": 10,
-        "x": 10,
-        "y": 0
-      },
-      "id": 12,
-      "legend": {
-        "alignAsTable": true,
-        "avg": false,
-        "current": true,
-        "max": true,
-        "min": false,
-        "rightSide": false,
-        "show": true,
-        "sideWidth": 480,
-        "sort": "max",
-        "sortDesc": true,
-        "total": false,
-        "values": true
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "connected",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "sum(kafka_consumergroup_lag{instance=\"$instance\",topic=~\"$topic\"}) by (consumergroup, topic) ",
-          "format": "time_series",
-          "instant": false,
-          "interval": "",
-          "intervalFactor": 1,
-          "legendFormat": " {{topic}} ({{consumergroup}})",
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Lag by  Consumer Group",
-      "tooltip": {
-        "shared": true,
-        "sort": 2,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": "",
-          "logBase": 1,
-          "max": null,
-          "min": "0",
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fill": 0,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 10,
-        "w": 10,
-        "x": 0,
-        "y": 10
-      },
-      "id": 16,
-      "legend": {
-        "alignAsTable": true,
-        "avg": false,
-        "current": true,
-        "max": true,
-        "min": false,
-        "rightSide": false,
-        "show": true,
-        "sideWidth": 480,
-        "total": false,
-        "values": true
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "connected",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "sum(delta(kafka_topic_partition_current_offset{instance=~'$instance', topic=~\"$topic\"}[5m])/5) by (topic)",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "{{topic}}",
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Messages produced per minute",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fill": 0,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 10,
-        "w": 10,
-        "x": 10,
-        "y": 10
-      },
-      "id": 18,
-      "legend": {
-        "alignAsTable": true,
-        "avg": false,
-        "current": true,
-        "max": true,
-        "min": false,
-        "rightSide": false,
-        "show": true,
-        "sideWidth": 480,
-        "sort": "current",
-        "sortDesc": true,
-        "total": false,
-        "values": true
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "connected",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "sum(delta(kafka_consumergroup_current_offset{instance=~'$instance',topic=~\"$topic\"}[5m])/5) by (consumergroup, topic)",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": " {{topic}} ({{consumergroup}})",
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Messages consumed per minute",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": true,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 7,
-        "w": 20,
-        "x": 0,
-        "y": 20
-      },
-      "id": 8,
-      "legend": {
-        "alignAsTable": true,
-        "avg": false,
-        "current": true,
-        "max": false,
-        "min": false,
-        "rightSide": true,
-        "show": true,
-        "sideWidth": 420,
-        "total": false,
-        "values": true
-      },
-      "lines": false,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "sum by(topic) (kafka_topic_partitions{instance=\"$instance\",topic=~\"$topic\"})",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "{{topic}}",
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Partitions per Topic",
-      "tooltip": {
-        "shared": false,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "series",
-        "name": null,
-        "show": false,
-        "values": [
-          "current"
-        ]
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    }
-  ],
-  "refresh": "5s",
-  "schemaVersion": 19,
-  "style": "dark",
-  "tags": [],
-  "templating": {
-    "list": [
-      {
-        "allValue": null,
-        "current": {
-          "text": "osm-kafka-exporter-service",
-          "value": "osm-kafka-exporter-service"
-        },
-        "datasource": "prometheus - Juju generated source",
-        "definition": "",
-        "hide": 0,
-        "includeAll": false,
-        "label": "Job",
-        "multi": false,
-        "name": "job",
-        "options": [],
-        "query": "label_values(kafka_consumergroup_current_offset, job)",
-        "refresh": 1,
-        "regex": "",
-        "skipUrlSync": false,
-        "sort": 0,
-        "tagValuesQuery": "",
-        "tags": [],
-        "tagsQuery": "",
-        "type": "query",
-        "useTags": false
-      },
-      {
-        "allValue": null,
-        "datasource": "prometheus - Juju generated source",
-        "definition": "",
-        "hide": 0,
-        "includeAll": false,
-        "label": "Instance",
-        "multi": false,
-        "name": "instance",
-        "options": [],
-        "query": "label_values(kafka_consumergroup_current_offset{job=~\"$job\"}, instance)",
-        "refresh": 1,
-        "regex": "",
-        "skipUrlSync": false,
-        "sort": 0,
-        "tagValuesQuery": "",
-        "tags": [],
-        "tagsQuery": "",
-        "type": "query",
-        "useTags": false
-      },
-      {
-        "allValue": null,
-        "current": {
-          "tags": [],
-          "text": "All",
-          "value": [
-            "$__all"
-          ]
-        },
-        "datasource": "prometheus - Juju generated source",
-        "definition": "",
-        "hide": 0,
-        "includeAll": true,
-        "label": "Topic",
-        "multi": true,
-        "name": "topic",
-        "options": [],
-        "query": "label_values(kafka_topic_partition_current_offset{instance='$instance',topic!='__consumer_offsets',topic!='--kafka'}, topic)",
-        "refresh": 1,
-        "regex": "",
-        "skipUrlSync": false,
-        "sort": 1,
-        "tagValuesQuery": "",
-        "tags": [],
-        "tagsQuery": "topic",
-        "type": "query",
-        "useTags": false
-      }
-    ]
-  },
-  "time": {
-    "from": "now-1h",
-    "to": "now"
-  },
-  "timepicker": {
-    "refresh_intervals": [
-      "5s",
-      "10s",
-      "30s",
-      "1m",
-      "5m",
-      "15m",
-      "30m",
-      "1h",
-      "2h",
-      "1d"
-    ],
-    "time_options": [
-      "5m",
-      "15m",
-      "1h",
-      "6h",
-      "12h",
-      "24h",
-      "2d",
-      "7d",
-      "30d"
-    ]
-  },
-  "timezone": "browser",
-  "title": "Kafka",
-  "uid": "jwPKIsniz",
-  "version": 2
-}
diff --git a/installers/charm/kafka-exporter/tests/__init__.py b/installers/charm/kafka-exporter/tests/__init__.py
deleted file mode 100644 (file)
index 90dc417..0000000
+++ /dev/null
@@ -1,39 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-"""Init mocking for unit tests."""
-
-import sys
-
-import mock
-
-
-class OCIImageResourceErrorMock(Exception):
-    pass
-
-
-sys.path.append("src")
-
-oci_image = mock.MagicMock()
-oci_image.OCIImageResourceError = OCIImageResourceErrorMock
-sys.modules["oci_image"] = oci_image
-sys.modules["oci_image"].OCIImageResource().fetch.return_value = {}
diff --git a/installers/charm/kafka-exporter/tests/test_charm.py b/installers/charm/kafka-exporter/tests/test_charm.py
deleted file mode 100644 (file)
index c00943b..0000000
+++ /dev/null
@@ -1,554 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-import sys
-from typing import NoReturn
-import unittest
-
-
-from charm import KafkaExporterCharm
-from ops.model import ActiveStatus, BlockedStatus
-from ops.testing import Harness
-
-
-class TestCharm(unittest.TestCase):
-    """Kafka Exporter Charm unit tests."""
-
-    def setUp(self) -> NoReturn:
-        """Test setup"""
-        self.image_info = sys.modules["oci_image"].OCIImageResource().fetch()
-        self.harness = Harness(KafkaExporterCharm)
-        self.harness.set_leader(is_leader=True)
-        self.harness.begin()
-        self.config = {
-            "ingress_whitelist_source_range": "",
-            "tls_secret_name": "",
-            "site_url": "https://kafka-exporter.192.168.100.100.nip.io",
-            "cluster_issuer": "vault-issuer",
-        }
-        self.harness.update_config(self.config)
-
-    def test_config_changed_no_relations(
-        self,
-    ) -> NoReturn:
-        """Test ingress resources without HTTP."""
-
-        self.harness.charm.on.config_changed.emit()
-
-        # Assertions
-        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
-        print(self.harness.charm.unit.status.message)
-        self.assertTrue(
-            all(
-                relation in self.harness.charm.unit.status.message
-                for relation in ["kafka"]
-            )
-        )
-
-    def test_config_changed_non_leader(
-        self,
-    ) -> NoReturn:
-        """Test ingress resources without HTTP."""
-        self.harness.set_leader(is_leader=False)
-        self.harness.charm.on.config_changed.emit()
-
-        # Assertions
-        self.assertIsInstance(self.harness.charm.unit.status, ActiveStatus)
-
-    def test_with_relations(
-        self,
-    ) -> NoReturn:
-        "Test with relations"
-        self.initialize_kafka_relation()
-
-        # Verifying status
-        self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
-
-    def initialize_kafka_relation(self):
-        kafka_relation_id = self.harness.add_relation("kafka", "kafka")
-        self.harness.add_relation_unit(kafka_relation_id, "kafka/0")
-        self.harness.update_relation_data(
-            kafka_relation_id, "kafka", {"host": "kafka", "port": 9092}
-        )
-
-
-if __name__ == "__main__":
-    unittest.main()
-
-
-# class TestCharm(unittest.TestCase):
-#     """Kafka Exporter Charm unit tests."""
-#
-#     def setUp(self) -> NoReturn:
-#         """Test setup"""
-#         self.harness = Harness(KafkaExporterCharm)
-#         self.harness.set_leader(is_leader=True)
-#         self.harness.begin()
-#
-#     def test_on_start_without_relations(self) -> NoReturn:
-#         """Test installation without any relation."""
-#         self.harness.charm.on.start.emit()
-#
-#         # Verifying status
-#         self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
-#
-#         # Verifying status message
-#         self.assertGreater(len(self.harness.charm.unit.status.message), 0)
-#         self.assertTrue(
-#             self.harness.charm.unit.status.message.startswith("Waiting for ")
-#         )
-#         self.assertIn("kafka", self.harness.charm.unit.status.message)
-#         self.assertTrue(self.harness.charm.unit.status.message.endswith(" relation"))
-#
-#     def test_on_start_with_relations_without_http(self) -> NoReturn:
-#         """Test deployment."""
-#         expected_result = {
-#             "version": 3,
-#             "containers": [
-#                 {
-#                     "name": "kafka-exporter",
-#                     "imageDetails": self.harness.charm.image.fetch(),
-#                     "imagePullPolicy": "Always",
-#                     "ports": [
-#                         {
-#                             "name": "kafka-exporter",
-#                             "containerPort": 9308,
-#                             "protocol": "TCP",
-#                         }
-#                     ],
-#                     "envConfig": {},
-#                     "command": ["kafka_exporter", "--kafka.server=kafka:9090"],
-#                     "kubernetes": {
-#                         "readinessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": 9308,
-#                             },
-#                             "initialDelaySeconds": 10,
-#                             "periodSeconds": 10,
-#                             "timeoutSeconds": 5,
-#                             "successThreshold": 1,
-#                             "failureThreshold": 3,
-#                         },
-#                         "livenessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": 9308,
-#                             },
-#                             "initialDelaySeconds": 60,
-#                             "timeoutSeconds": 30,
-#                             "failureThreshold": 10,
-#                         },
-#                     },
-#                 },
-#             ],
-#             "kubernetesResources": {"ingressResources": []},
-#         }
-#
-#         self.harness.charm.on.start.emit()
-#
-#         # Initializing the kafka relation
-#         relation_id = self.harness.add_relation("kafka", "kafka")
-#         self.harness.add_relation_unit(relation_id, "kafka/0")
-#         self.harness.update_relation_data(
-#             relation_id,
-#             "kafka/0",
-#             {
-#                 "host": "kafka",
-#                 "port": "9090",
-#             },
-#         )
-#
-#         # Verifying status
-#         self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
-#
-#         pod_spec, _ = self.harness.get_pod_spec()
-#
-#         self.assertDictEqual(expected_result, pod_spec)
-#
-#     def test_ingress_resources_with_http(self) -> NoReturn:
-#         """Test ingress resources with HTTP."""
-#         expected_result = {
-#             "version": 3,
-#             "containers": [
-#                 {
-#                     "name": "kafka-exporter",
-#                     "imageDetails": self.harness.charm.image.fetch(),
-#                     "imagePullPolicy": "Always",
-#                     "ports": [
-#                         {
-#                             "name": "kafka-exporter",
-#                             "containerPort": 9308,
-#                             "protocol": "TCP",
-#                         }
-#                     ],
-#                     "envConfig": {},
-#                     "command": ["kafka_exporter", "--kafka.server=kafka:9090"],
-#                     "kubernetes": {
-#                         "readinessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": 9308,
-#                             },
-#                             "initialDelaySeconds": 10,
-#                             "periodSeconds": 10,
-#                             "timeoutSeconds": 5,
-#                             "successThreshold": 1,
-#                             "failureThreshold": 3,
-#                         },
-#                         "livenessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": 9308,
-#                             },
-#                             "initialDelaySeconds": 60,
-#                             "timeoutSeconds": 30,
-#                             "failureThreshold": 10,
-#                         },
-#                     },
-#                 },
-#             ],
-#             "kubernetesResources": {
-#                 "ingressResources": [
-#                     {
-#                         "name": "kafka-exporter-ingress",
-#                         "annotations": {
-#                             "nginx.ingress.kubernetes.io/ssl-redirect": "false",
-#                         },
-#                         "spec": {
-#                             "rules": [
-#                                 {
-#                                     "host": "kafka-exporter",
-#                                     "http": {
-#                                         "paths": [
-#                                             {
-#                                                 "path": "/",
-#                                                 "backend": {
-#                                                     "serviceName": "kafka-exporter",
-#                                                     "servicePort": 9308,
-#                                                 },
-#                                             }
-#                                         ]
-#                                     },
-#                                 }
-#                             ]
-#                         },
-#                     }
-#                 ],
-#             },
-#         }
-#
-#         self.harness.charm.on.start.emit()
-#
-#         # Initializing the kafka relation
-#         relation_id = self.harness.add_relation("kafka", "kafka")
-#         self.harness.add_relation_unit(relation_id, "kafka/0")
-#         self.harness.update_relation_data(
-#             relation_id,
-#             "kafka/0",
-#             {
-#                 "host": "kafka",
-#                 "port": "9090",
-#             },
-#         )
-#
-#         self.harness.update_config({"site_url": "http://kafka-exporter"})
-#
-#         pod_spec, _ = self.harness.get_pod_spec()
-#
-#         self.assertDictEqual(expected_result, pod_spec)
-#
-#     def test_ingress_resources_with_https(self) -> NoReturn:
-#         """Test ingress resources with HTTPS."""
-#         expected_result = {
-#             "version": 3,
-#             "containers": [
-#                 {
-#                     "name": "kafka-exporter",
-#                     "imageDetails": self.harness.charm.image.fetch(),
-#                     "imagePullPolicy": "Always",
-#                     "ports": [
-#                         {
-#                             "name": "kafka-exporter",
-#                             "containerPort": 9308,
-#                             "protocol": "TCP",
-#                         }
-#                     ],
-#                     "envConfig": {},
-#                     "command": ["kafka_exporter", "--kafka.server=kafka:9090"],
-#                     "kubernetes": {
-#                         "readinessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": 9308,
-#                             },
-#                             "initialDelaySeconds": 10,
-#                             "periodSeconds": 10,
-#                             "timeoutSeconds": 5,
-#                             "successThreshold": 1,
-#                             "failureThreshold": 3,
-#                         },
-#                         "livenessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": 9308,
-#                             },
-#                             "initialDelaySeconds": 60,
-#                             "timeoutSeconds": 30,
-#                             "failureThreshold": 10,
-#                         },
-#                     },
-#                 },
-#             ],
-#             "kubernetesResources": {
-#                 "ingressResources": [
-#                     {
-#                         "name": "kafka-exporter-ingress",
-#                         "annotations": {},
-#                         "spec": {
-#                             "rules": [
-#                                 {
-#                                     "host": "kafka-exporter",
-#                                     "http": {
-#                                         "paths": [
-#                                             {
-#                                                 "path": "/",
-#                                                 "backend": {
-#                                                     "serviceName": "kafka-exporter",
-#                                                     "servicePort": 9308,
-#                                                 },
-#                                             }
-#                                         ]
-#                                     },
-#                                 }
-#                             ],
-#                             "tls": [
-#                                 {
-#                                     "hosts": ["kafka-exporter"],
-#                                     "secretName": "kafka-exporter",
-#                                 }
-#                             ],
-#                         },
-#                     }
-#                 ],
-#             },
-#         }
-#
-#         self.harness.charm.on.start.emit()
-#
-#         # Initializing the kafka relation
-#         relation_id = self.harness.add_relation("kafka", "kafka")
-#         self.harness.add_relation_unit(relation_id, "kafka/0")
-#         self.harness.update_relation_data(
-#             relation_id,
-#             "kafka/0",
-#             {
-#                 "host": "kafka",
-#                 "port": "9090",
-#             },
-#         )
-#
-#         self.harness.update_config(
-#             {
-#                 "site_url": "https://kafka-exporter",
-#                 "tls_secret_name": "kafka-exporter",
-#             }
-#         )
-#
-#         pod_spec, _ = self.harness.get_pod_spec()
-#
-#         self.assertDictEqual(expected_result, pod_spec)
-#
-#     def test_ingress_resources_with_https_and_ingress_whitelist(self) -> NoReturn:
-#         """Test ingress resources with HTTPS and ingress whitelist."""
-#         expected_result = {
-#             "version": 3,
-#             "containers": [
-#                 {
-#                     "name": "kafka-exporter",
-#                     "imageDetails": self.harness.charm.image.fetch(),
-#                     "imagePullPolicy": "Always",
-#                     "ports": [
-#                         {
-#                             "name": "kafka-exporter",
-#                             "containerPort": 9308,
-#                             "protocol": "TCP",
-#                         }
-#                     ],
-#                     "envConfig": {},
-#                     "command": ["kafka_exporter", "--kafka.server=kafka:9090"],
-#                     "kubernetes": {
-#                         "readinessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": 9308,
-#                             },
-#                             "initialDelaySeconds": 10,
-#                             "periodSeconds": 10,
-#                             "timeoutSeconds": 5,
-#                             "successThreshold": 1,
-#                             "failureThreshold": 3,
-#                         },
-#                         "livenessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": 9308,
-#                             },
-#                             "initialDelaySeconds": 60,
-#                             "timeoutSeconds": 30,
-#                             "failureThreshold": 10,
-#                         },
-#                     },
-#                 },
-#             ],
-#             "kubernetesResources": {
-#                 "ingressResources": [
-#                     {
-#                         "name": "kafka-exporter-ingress",
-#                         "annotations": {
-#                             "nginx.ingress.kubernetes.io/whitelist-source-range": "0.0.0.0/0",
-#                         },
-#                         "spec": {
-#                             "rules": [
-#                                 {
-#                                     "host": "kafka-exporter",
-#                                     "http": {
-#                                         "paths": [
-#                                             {
-#                                                 "path": "/",
-#                                                 "backend": {
-#                                                     "serviceName": "kafka-exporter",
-#                                                     "servicePort": 9308,
-#                                                 },
-#                                             }
-#                                         ]
-#                                     },
-#                                 }
-#                             ],
-#                             "tls": [
-#                                 {
-#                                     "hosts": ["kafka-exporter"],
-#                                     "secretName": "kafka-exporter",
-#                                 }
-#                             ],
-#                         },
-#                     }
-#                 ],
-#             },
-#         }
-#
-#         self.harness.charm.on.start.emit()
-#
-#         # Initializing the kafka relation
-#         relation_id = self.harness.add_relation("kafka", "kafka")
-#         self.harness.add_relation_unit(relation_id, "kafka/0")
-#         self.harness.update_relation_data(
-#             relation_id,
-#             "kafka/0",
-#             {
-#                 "host": "kafka",
-#                 "port": "9090",
-#             },
-#         )
-#
-#         self.harness.update_config(
-#             {
-#                 "site_url": "https://kafka-exporter",
-#                 "tls_secret_name": "kafka-exporter",
-#                 "ingress_whitelist_source_range": "0.0.0.0/0",
-#             }
-#         )
-#
-#         pod_spec, _ = self.harness.get_pod_spec()
-#
-#         self.assertDictEqual(expected_result, pod_spec)
-#
-#     def test_on_kafka_unit_relation_changed(self) -> NoReturn:
-#         """Test to see if kafka relation is updated."""
-#         self.harness.charm.on.start.emit()
-#
-#         relation_id = self.harness.add_relation("kafka", "kafka")
-#         self.harness.add_relation_unit(relation_id, "kafka/0")
-#         self.harness.update_relation_data(
-#             relation_id,
-#             "kafka/0",
-#             {
-#                 "host": "kafka",
-#                 "port": "9090",
-#             },
-#         )
-#
-#         # Verifying status
-#         self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
-#
-#     def test_publish_target_info(self) -> NoReturn:
-#         """Test to see if target relation is updated."""
-#         expected_result = {
-#             "hostname": "kafka-exporter",
-#             "port": "9308",
-#             "metrics_path": "/metrics",
-#             "scrape_interval": "30s",
-#             "scrape_timeout": "15s",
-#         }
-#
-#         self.harness.charm.on.start.emit()
-#
-#         relation_id = self.harness.add_relation("prometheus-scrape", "prometheus")
-#         self.harness.add_relation_unit(relation_id, "prometheus/0")
-#         relation_data = self.harness.get_relation_data(relation_id, "kafka-exporter/0")
-#
-#         self.assertDictEqual(expected_result, relation_data)
-#
-#     def test_publish_target_info_with_site_url(self) -> NoReturn:
-#         """Test to see if target relation is updated."""
-#         expected_result = {
-#             "hostname": "kafka-exporter-osm",
-#             "port": "80",
-#             "metrics_path": "/metrics",
-#             "scrape_interval": "30s",
-#             "scrape_timeout": "15s",
-#         }
-#
-#         self.harness.charm.on.start.emit()
-#
-#         self.harness.update_config({"site_url": "http://kafka-exporter-osm"})
-#
-#         relation_id = self.harness.add_relation("prometheus-scrape", "prometheus")
-#         self.harness.add_relation_unit(relation_id, "prometheus/0")
-#         relation_data = self.harness.get_relation_data(relation_id, "kafka-exporter/0")
-#
-#         self.assertDictEqual(expected_result, relation_data)
-#
-#     def test_publish_dashboard_info(self) -> NoReturn:
-#         """Test to see if dashboard relation is updated."""
-#         self.harness.charm.on.start.emit()
-#
-#         relation_id = self.harness.add_relation("grafana-dashboard", "grafana")
-#         self.harness.add_relation_unit(relation_id, "grafana/0")
-#         relation_data = self.harness.get_relation_data(relation_id, "kafka-exporter/0")
-#
-#         self.assertTrue("dashboard" in relation_data)
-#         self.assertTrue(len(relation_data["dashboard"]) > 0)
-#
-#
-# if __name__ == "__main__":
-#     unittest.main()
diff --git a/installers/charm/kafka-exporter/tests/test_pod_spec.py b/installers/charm/kafka-exporter/tests/test_pod_spec.py
deleted file mode 100644 (file)
index ad0e412..0000000
+++ /dev/null
@@ -1,509 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-from typing import NoReturn
-import unittest
-
-import pod_spec
-
-
-class TestPodSpec(unittest.TestCase):
-    """Pod spec unit tests."""
-
-    def test_make_pod_ports(self) -> NoReturn:
-        """Testing make pod ports."""
-        port = 9308
-
-        expected_result = [
-            {
-                "name": "kafka-exporter",
-                "containerPort": port,
-                "protocol": "TCP",
-            }
-        ]
-
-        pod_ports = pod_spec._make_pod_ports(port)
-
-        self.assertListEqual(expected_result, pod_ports)
-
-    def test_make_pod_envconfig(self) -> NoReturn:
-        """Teting make pod envconfig."""
-        config = {}
-        relation_state = {}
-
-        expected_result = {}
-
-        pod_envconfig = pod_spec._make_pod_envconfig(config, relation_state)
-
-        self.assertDictEqual(expected_result, pod_envconfig)
-
-    def test_make_pod_ingress_resources_without_site_url(self) -> NoReturn:
-        """Testing make pod ingress resources without site_url."""
-        config = {
-            "cluster_issuer": "",
-            "site_url": "",
-        }
-        app_name = "kafka-exporter"
-        port = 9308
-
-        pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-            config, app_name, port
-        )
-
-        self.assertIsNone(pod_ingress_resources)
-
-    def test_make_pod_ingress_resources(self) -> NoReturn:
-        """Testing make pod ingress resources."""
-        config = {
-            "cluster_issuer": "",
-            "site_url": "http://kafka-exporter",
-            "ingress_whitelist_source_range": "",
-        }
-        app_name = "kafka-exporter"
-        port = 9308
-
-        expected_result = [
-            {
-                "name": f"{app_name}-ingress",
-                "annotations": {
-                    "nginx.ingress.kubernetes.io/ssl-redirect": "false",
-                },
-                "spec": {
-                    "rules": [
-                        {
-                            "host": app_name,
-                            "http": {
-                                "paths": [
-                                    {
-                                        "path": "/",
-                                        "backend": {
-                                            "serviceName": app_name,
-                                            "servicePort": port,
-                                        },
-                                    }
-                                ]
-                            },
-                        }
-                    ]
-                },
-            }
-        ]
-
-        pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-            config, app_name, port
-        )
-
-        self.assertListEqual(expected_result, pod_ingress_resources)
-
-    def test_make_pod_ingress_resources_with_whitelist_source_range(self) -> NoReturn:
-        """Testing make pod ingress resources with whitelist_source_range."""
-        config = {
-            "site_url": "http://kafka-exporter",
-            "cluster_issuer": "",
-            "ingress_whitelist_source_range": "0.0.0.0/0",
-        }
-        app_name = "kafka-exporter"
-        port = 9308
-
-        expected_result = [
-            {
-                "name": f"{app_name}-ingress",
-                "annotations": {
-                    "nginx.ingress.kubernetes.io/ssl-redirect": "false",
-                    "nginx.ingress.kubernetes.io/whitelist-source-range": config[
-                        "ingress_whitelist_source_range"
-                    ],
-                },
-                "spec": {
-                    "rules": [
-                        {
-                            "host": app_name,
-                            "http": {
-                                "paths": [
-                                    {
-                                        "path": "/",
-                                        "backend": {
-                                            "serviceName": app_name,
-                                            "servicePort": port,
-                                        },
-                                    }
-                                ]
-                            },
-                        }
-                    ]
-                },
-            }
-        ]
-
-        pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-            config, app_name, port
-        )
-
-        self.assertListEqual(expected_result, pod_ingress_resources)
-
-    def test_make_pod_ingress_resources_with_https(self) -> NoReturn:
-        """Testing make pod ingress resources with HTTPs."""
-        config = {
-            "site_url": "https://kafka-exporter",
-            "max_file_size": 0,
-            "cluster_issuer": "",
-            "ingress_whitelist_source_range": "",
-            "tls_secret_name": "",
-        }
-        app_name = "kafka-exporter"
-        port = 9308
-
-        expected_result = [
-            {
-                "name": f"{app_name}-ingress",
-                "annotations": {},
-                "spec": {
-                    "rules": [
-                        {
-                            "host": app_name,
-                            "http": {
-                                "paths": [
-                                    {
-                                        "path": "/",
-                                        "backend": {
-                                            "serviceName": app_name,
-                                            "servicePort": port,
-                                        },
-                                    }
-                                ]
-                            },
-                        }
-                    ],
-                    "tls": [{"hosts": [app_name]}],
-                },
-            }
-        ]
-
-        pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-            config, app_name, port
-        )
-
-        self.assertListEqual(expected_result, pod_ingress_resources)
-
-    def test_make_pod_ingress_resources_with_https_tls_secret_name(self) -> NoReturn:
-        """Testing make pod ingress resources with HTTPs and TLS secret name."""
-        config = {
-            "site_url": "https://kafka-exporter",
-            "max_file_size": 0,
-            "cluster_issuer": "",
-            "ingress_whitelist_source_range": "",
-            "tls_secret_name": "secret_name",
-        }
-        app_name = "kafka-exporter"
-        port = 9308
-
-        expected_result = [
-            {
-                "name": f"{app_name}-ingress",
-                "annotations": {},
-                "spec": {
-                    "rules": [
-                        {
-                            "host": app_name,
-                            "http": {
-                                "paths": [
-                                    {
-                                        "path": "/",
-                                        "backend": {
-                                            "serviceName": app_name,
-                                            "servicePort": port,
-                                        },
-                                    }
-                                ]
-                            },
-                        }
-                    ],
-                    "tls": [
-                        {"hosts": [app_name], "secretName": config["tls_secret_name"]}
-                    ],
-                },
-            }
-        ]
-
-        pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-            config, app_name, port
-        )
-
-        self.assertListEqual(expected_result, pod_ingress_resources)
-
-    def test_make_readiness_probe(self) -> NoReturn:
-        """Testing make readiness probe."""
-        port = 9308
-
-        expected_result = {
-            "httpGet": {
-                "path": "/api/health",
-                "port": port,
-            },
-            "initialDelaySeconds": 10,
-            "periodSeconds": 10,
-            "timeoutSeconds": 5,
-            "successThreshold": 1,
-            "failureThreshold": 3,
-        }
-
-        readiness_probe = pod_spec._make_readiness_probe(port)
-
-        self.assertDictEqual(expected_result, readiness_probe)
-
-    def test_make_liveness_probe(self) -> NoReturn:
-        """Testing make liveness probe."""
-        port = 9308
-
-        expected_result = {
-            "httpGet": {
-                "path": "/api/health",
-                "port": port,
-            },
-            "initialDelaySeconds": 60,
-            "timeoutSeconds": 30,
-            "failureThreshold": 10,
-        }
-
-        liveness_probe = pod_spec._make_liveness_probe(port)
-
-        self.assertDictEqual(expected_result, liveness_probe)
-
-    def test_make_pod_command(self) -> NoReturn:
-        """Testing make pod command."""
-        relation = {
-            "kakfa_host": "kafka",
-            "kafka_port": "9090",
-        }
-
-        expected_result = [
-            "kafka_exporter",
-            "--kafka.server={}:{}".format(
-                relation.get("kafka_host"), relation.get("kafka_port")
-            ),
-        ]
-
-        pod_envconfig = pod_spec._make_pod_command(relation)
-
-        self.assertListEqual(expected_result, pod_envconfig)
-
-    def test_make_pod_spec(self) -> NoReturn:
-        """Testing make pod spec."""
-        image_info = {"upstream-source": "bitnami/kafka-exporter:latest"}
-        config = {
-            "site_url": "",
-            "cluster_issuer": "",
-        }
-        relation_state = {
-            "kafka_host": "kafka",
-            "kafka_port": "9090",
-        }
-        app_name = "kafka-exporter"
-        port = 9308
-
-        expected_result = {
-            "version": 3,
-            "containers": [
-                {
-                    "name": app_name,
-                    "imageDetails": image_info,
-                    "imagePullPolicy": "Always",
-                    "ports": [
-                        {
-                            "name": app_name,
-                            "containerPort": port,
-                            "protocol": "TCP",
-                        }
-                    ],
-                    "envConfig": {},
-                    "command": ["kafka_exporter", "--kafka.server=kafka:9090"],
-                    "kubernetes": {
-                        "readinessProbe": {
-                            "httpGet": {
-                                "path": "/api/health",
-                                "port": port,
-                            },
-                            "initialDelaySeconds": 10,
-                            "periodSeconds": 10,
-                            "timeoutSeconds": 5,
-                            "successThreshold": 1,
-                            "failureThreshold": 3,
-                        },
-                        "livenessProbe": {
-                            "httpGet": {
-                                "path": "/api/health",
-                                "port": port,
-                            },
-                            "initialDelaySeconds": 60,
-                            "timeoutSeconds": 30,
-                            "failureThreshold": 10,
-                        },
-                    },
-                }
-            ],
-            "kubernetesResources": {"ingressResources": []},
-        }
-
-        spec = pod_spec.make_pod_spec(
-            image_info, config, relation_state, app_name, port
-        )
-
-        self.assertDictEqual(expected_result, spec)
-
-    def test_make_pod_spec_with_ingress(self) -> NoReturn:
-        """Testing make pod spec."""
-        image_info = {"upstream-source": "bitnami/kafka-exporter:latest"}
-        config = {
-            "site_url": "https://kafka-exporter",
-            "cluster_issuer": "",
-            "tls_secret_name": "kafka-exporter",
-            "max_file_size": 0,
-            "ingress_whitelist_source_range": "0.0.0.0/0",
-        }
-        relation_state = {
-            "kafka_host": "kafka",
-            "kafka_port": "9090",
-        }
-        app_name = "kafka-exporter"
-        port = 9308
-
-        expected_result = {
-            "version": 3,
-            "containers": [
-                {
-                    "name": app_name,
-                    "imageDetails": image_info,
-                    "imagePullPolicy": "Always",
-                    "ports": [
-                        {
-                            "name": app_name,
-                            "containerPort": port,
-                            "protocol": "TCP",
-                        }
-                    ],
-                    "envConfig": {},
-                    "command": ["kafka_exporter", "--kafka.server=kafka:9090"],
-                    "kubernetes": {
-                        "readinessProbe": {
-                            "httpGet": {
-                                "path": "/api/health",
-                                "port": port,
-                            },
-                            "initialDelaySeconds": 10,
-                            "periodSeconds": 10,
-                            "timeoutSeconds": 5,
-                            "successThreshold": 1,
-                            "failureThreshold": 3,
-                        },
-                        "livenessProbe": {
-                            "httpGet": {
-                                "path": "/api/health",
-                                "port": port,
-                            },
-                            "initialDelaySeconds": 60,
-                            "timeoutSeconds": 30,
-                            "failureThreshold": 10,
-                        },
-                    },
-                }
-            ],
-            "kubernetesResources": {
-                "ingressResources": [
-                    {
-                        "name": "{}-ingress".format(app_name),
-                        "annotations": {
-                            "nginx.ingress.kubernetes.io/whitelist-source-range": config.get(
-                                "ingress_whitelist_source_range"
-                            ),
-                        },
-                        "spec": {
-                            "rules": [
-                                {
-                                    "host": app_name,
-                                    "http": {
-                                        "paths": [
-                                            {
-                                                "path": "/",
-                                                "backend": {
-                                                    "serviceName": app_name,
-                                                    "servicePort": port,
-                                                },
-                                            }
-                                        ]
-                                    },
-                                }
-                            ],
-                            "tls": [
-                                {
-                                    "hosts": [app_name],
-                                    "secretName": config.get("tls_secret_name"),
-                                }
-                            ],
-                        },
-                    }
-                ],
-            },
-        }
-
-        spec = pod_spec.make_pod_spec(
-            image_info, config, relation_state, app_name, port
-        )
-
-        self.assertDictEqual(expected_result, spec)
-
-    def test_make_pod_spec_without_image_info(self) -> NoReturn:
-        """Testing make pod spec without image_info."""
-        image_info = None
-        config = {
-            "site_url": "",
-            "cluster_issuer": "",
-        }
-        relation_state = {
-            "kafka_host": "kafka",
-            "kafka_port": "9090",
-        }
-        app_name = "kafka-exporter"
-        port = 9308
-
-        spec = pod_spec.make_pod_spec(
-            image_info, config, relation_state, app_name, port
-        )
-
-        self.assertIsNone(spec)
-
-    def test_make_pod_spec_without_relation_state(self) -> NoReturn:
-        """Testing make pod spec without relation_state."""
-        image_info = {"upstream-source": "bitnami/kafka-exporter:latest"}
-        config = {
-            "site_url": "",
-            "cluster_issuer": "",
-        }
-        relation_state = {}
-        app_name = "kafka-exporter"
-        port = 9308
-
-        with self.assertRaises(ValueError):
-            pod_spec.make_pod_spec(image_info, config, relation_state, app_name, port)
-
-
-if __name__ == "__main__":
-    unittest.main()
diff --git a/installers/charm/kafka-exporter/tox.ini b/installers/charm/kafka-exporter/tox.ini
deleted file mode 100644 (file)
index f3c9144..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-#######################################################################################
-
-[tox]
-envlist = black, cover, flake8, pylint, yamllint, safety
-skipsdist = true
-
-[tox:jenkins]
-toxworkdir = /tmp/.tox
-
-[testenv]
-basepython = python3.8
-setenv =
-  VIRTUAL_ENV={envdir}
-  PYTHONPATH = {toxinidir}:{toxinidir}/lib:{toxinidir}/src
-  PYTHONDONTWRITEBYTECODE = 1
-deps =  -r{toxinidir}/requirements.txt
-
-
-#######################################################################################
-[testenv:black]
-deps = black
-commands =
-        black --check --diff src/ tests/
-
-
-#######################################################################################
-[testenv:cover]
-deps =  {[testenv]deps}
-        -r{toxinidir}/requirements-test.txt
-        coverage
-        nose2
-commands =
-        sh -c 'rm -f nosetests.xml'
-        coverage erase
-        nose2 -C --coverage src
-        coverage report --omit='*tests*'
-        coverage html -d ./cover --omit='*tests*'
-        coverage xml -o coverage.xml --omit=*tests*
-whitelist_externals = sh
-
-
-#######################################################################################
-[testenv:flake8]
-deps =  flake8
-        flake8-import-order
-commands =
-        flake8 src/ tests/
-
-
-#######################################################################################
-[testenv:pylint]
-deps =  {[testenv]deps}
-        -r{toxinidir}/requirements-test.txt
-        pylint==2.10.2
-commands =
-    pylint -E src/ tests/
-
-
-#######################################################################################
-[testenv:safety]
-setenv =
-        LC_ALL=C.UTF-8
-        LANG=C.UTF-8
-deps =  {[testenv]deps}
-        safety
-commands =
-        - safety check --full-report
-
-
-#######################################################################################
-[testenv:yamllint]
-deps =  {[testenv]deps}
-        -r{toxinidir}/requirements-test.txt
-        yamllint
-commands = yamllint .
-
-#######################################################################################
-[testenv:build]
-passenv=HTTP_PROXY HTTPS_PROXY NO_PROXY
-whitelist_externals =
-  charmcraft
-  sh
-commands =
-  charmcraft pack
-  sh -c 'ubuntu_version=20.04; \
-        architectures="amd64-aarch64-arm64"; \
-        charm_name=`cat metadata.yaml | grep -E "^name: " | cut -f 2 -d " "`; \
-        mv $charm_name"_ubuntu-"$ubuntu_version-$architectures.charm $charm_name.charm'
-
-#######################################################################################
-[flake8]
-ignore =
-        W291,
-        W293,
-        W503,
-        E123,
-        E125,
-        E226,
-        E241,
-exclude =
-        .git,
-        __pycache__,
-        .tox,
-max-line-length = 120
-show-source = True
-builtins = _
-max-complexity = 10
-import-order-style = google
diff --git a/installers/charm/local_osm_bundle.yaml b/installers/charm/local_osm_bundle.yaml
deleted file mode 100644 (file)
index 6ab0df6..0000000
+++ /dev/null
@@ -1,215 +0,0 @@
-# Copyright 2020 Canonical Ltd.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-#     Unless required by applicable law or agreed to in writing, software
-#     distributed under the License is distributed on an "AS IS" BASIS,
-#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#     See the License for the specific language governing permissions and
-#     limitations under the License.
-name: osm
-bundle: kubernetes
-description: Local bundle for development
-applications:
-  zookeeper:
-    charm: zookeeper-k8s
-    channel: latest/edge
-    scale: 1
-    storage:
-      data: 100M
-    annotations:
-      gui-x: 0
-      gui-y: 500
-  mariadb:
-    charm: charmed-osm-mariadb-k8s
-    scale: 1
-    series: kubernetes
-    storage:
-      database: 50M
-    options:
-      password: manopw
-      root_password: osm4u
-      user: mano
-    annotations:
-      gui-x: -300
-      gui-y: -250
-  kafka:
-    charm: kafka-k8s
-    channel: latest/edge
-    scale: 1
-    trust: true
-    storage:
-      data: 100M
-    annotations:
-      gui-x: 0
-      gui-y: 250
-  mongodb:
-    charm: mongodb-k8s
-    channel: latest/stable
-    scale: 1
-    series: kubernetes
-    storage:
-      db: 50M
-    annotations:
-      gui-x: 0
-      gui-y: 0
-  nbi:
-    charm: ./nbi/osm-nbi.charm
-    scale: 1
-    resources:
-      image: opensourcemano/nbi:testing-daily
-    series: kubernetes
-    options:
-      database_commonkey: osm
-      auth_backend: keystone
-      log_level: DEBUG
-    annotations:
-      gui-x: 0
-      gui-y: -250
-  ro:
-    charm: ./ro/osm-ro.charm
-    scale: 1
-    resources:
-      image: opensourcemano/ro:testing-daily
-    series: kubernetes
-    options:
-      log_level: DEBUG
-    annotations:
-      gui-x: -300
-      gui-y: 250
-  ng-ui:
-    charm: ./ng-ui/osm-ng-ui.charm
-    scale: 1
-    resources:
-      image: opensourcemano/ng-ui:testing-daily
-    series: kubernetes
-    annotations:
-      gui-x: 600
-      gui-y: 0
-  lcm:
-    charm: ./lcm/osm-lcm.charm
-    scale: 1
-    resources:
-      image: opensourcemano/lcm:testing-daily
-    series: kubernetes
-    options:
-      database_commonkey: osm
-      log_level: DEBUG
-    annotations:
-      gui-x: -300
-      gui-y: 0
-  mon:
-    charm: ./mon/osm-mon.charm
-    scale: 1
-    resources:
-      image: opensourcemano/mon:testing-daily
-    series: kubernetes
-    options:
-      database_commonkey: osm
-      log_level: DEBUG
-      keystone_enabled: true
-    annotations:
-      gui-x: 300
-      gui-y: 0
-  pol:
-    charm: ./pol/osm-pol.charm
-    scale: 1
-    resources:
-      image: opensourcemano/pol:testing-daily
-    series: kubernetes
-    options:
-      log_level: DEBUG
-    annotations:
-      gui-x: -300
-      gui-y: 500
-  pla:
-    charm: ./pla/osm-pla.charm
-    scale: 1
-    resources:
-      image: opensourcemano/pla:testing-daily
-    series: kubernetes
-    options:
-      log_level: DEBUG
-    annotations:
-      gui-x: 600
-      gui-y: -250
-  prometheus:
-    charm: osm-prometheus
-    channel: latest/edge
-    scale: 1
-    series: kubernetes
-    storage:
-      data: 50M
-    options:
-      default-target: "mon:8000"
-    annotations:
-      gui-x: 300
-      gui-y: 250
-  grafana:
-    charm: osm-grafana
-    channel: latest/edge
-    scale: 1
-    series: kubernetes
-    annotations:
-      gui-x: 300
-      gui-y: 500
-  keystone:
-    charm: osm-keystone
-    channel: latest/edge
-    resources:
-      keystone-image: opensourcemano/keystone:testing-daily
-    scale: 1
-    annotations:
-      gui-x: 300
-      gui-y: -250
-relations:
-  - - grafana:prometheus
-    - prometheus:prometheus
-  - - kafka:zookeeper
-    - zookeeper:zookeeper
-  - - keystone:db
-    - mariadb:mysql
-  - - lcm:kafka
-    - kafka:kafka
-  - - lcm:mongodb
-    - mongodb:database
-  - - ro:ro
-    - lcm:ro
-  - - ro:kafka
-    - kafka:kafka
-  - - ro:mongodb
-    - mongodb:database
-  - - pol:kafka
-    - kafka:kafka
-  - - pol:mongodb
-    - mongodb:database
-  - - mon:mongodb
-    - mongodb:database
-  - - mon:kafka
-    - kafka:kafka
-  - - pla:kafka
-    - kafka:kafka
-  - - pla:mongodb
-    - mongodb:database
-  - - nbi:mongodb
-    - mongodb:database
-  - - nbi:kafka
-    - kafka:kafka
-  - - nbi:prometheus
-    - prometheus:prometheus
-  - - nbi:keystone
-    - keystone:keystone
-  - - mon:prometheus
-    - prometheus:prometheus
-  - - ng-ui:nbi
-    - nbi:nbi
-  - - mon:keystone
-    - keystone:keystone
-  - - mariadb:mysql
-    - pol:mysql
-  - - grafana:db
-    - mariadb:mysql
diff --git a/installers/charm/local_osm_bundle_proxy.yaml b/installers/charm/local_osm_bundle_proxy.yaml
deleted file mode 100644 (file)
index d328522..0000000
+++ /dev/null
@@ -1,200 +0,0 @@
-# Copyright 2020 Canonical Ltd.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-#     Unless required by applicable law or agreed to in writing, software
-#     distributed under the License is distributed on an "AS IS" BASIS,
-#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#     See the License for the specific language governing permissions and
-#     limitations under the License.
-description: Single instance OSM bundle
-bundle: kubernetes
-variables:
-  proxy: &proxy http://91.189.89.11:3128
-  no-proxy: &no_proxy 127.0.0.1,localhost,::1,10.131.15.1/24,10.152.183.0/24,10.1.0.0/16
-applications:
-  zookeeper-k8s:
-    charm: "cs:~charmed-osm/zookeeper-k8s"
-    channel: "stable"
-    scale: 1
-    series: kubernetes
-    storage:
-      database: 100M
-    annotations:
-      gui-x: 0
-      gui-y: 550
-  mariadb-k8s:
-    charm: "cs:~charmed-osm/mariadb-k8s"
-    channel: "stable"
-    scale: 1
-    series: kubernetes
-    storage:
-      database: 50M
-    options:
-      password: manopw
-      root_password: osm4u
-      user: mano
-    annotations:
-      gui-x: -250
-      gui-y: -200
-  kafka-k8s:
-    charm: "cs:~charmed-osm/kafka-k8s"
-    channel: "stable"
-    scale: 1
-    series: kubernetes
-    storage:
-      database: 100M
-    annotations:
-      gui-x: 0
-      gui-y: 300
-  mongodb-k8s:
-    charm: "cs:~charmed-osm/mongodb-k8s"
-    channel: "stable"
-    scale: 1
-    series: kubernetes
-    storage:
-      database: 50M
-    options:
-      replica-set: rs0
-      namespace: osm
-      enable-sidecar: true
-    annotations:
-      gui-x: 0
-      gui-y: 50
-  nbi:
-    charm: "./nbi/build"
-    scale: 1
-    series: kubernetes
-    options:
-      database_commonkey: osm
-      auth_backend: keystone
-    annotations:
-      gui-x: 0
-      gui-y: -200
-  ro:
-    charm: "./ro/build"
-    scale: 1
-    series: kubernetes
-    annotations:
-      gui-x: -250
-      gui-y: 300
-  ng-ui:
-    charm: "./ng-ui/build"
-    scale: 1
-    series: kubernetes
-    annotations:
-      gui-x: 500
-      gui-y: 100
-  lcm:
-    charm: "./lcm/build"
-    scale: 1
-    series: kubernetes
-    options:
-      database_commonkey: osm
-      vca_model_config_no_proxy: *no_proxy
-      vca_model_config_juju_no_proxy: *no_proxy
-      vca_model_config_apt_no_proxy: *no_proxy
-      vca_model_config_juju_http_proxy: *proxy
-      vca_model_config_juju_https_proxy: *proxy
-      vca_model_config_apt_http_proxy: *proxy
-      vca_model_config_apt_https_proxy: *proxy
-      vca_model_config_snap_http_proxy: *proxy
-      vca_model_config_snap_https_proxy: *proxy
-    annotations:
-      gui-x: -250
-      gui-y: 50
-  mon:
-    charm: "./mon/build"
-    scale: 1
-    series: kubernetes
-    options:
-      database_commonkey: osm
-    annotations:
-      gui-x: 250
-      gui-y: 50
-  pol:
-    charm: "./pol/build"
-    scale: 1
-    series: kubernetes
-    annotations:
-      gui-x: -250
-      gui-y: 550
-  pla:
-    charm: "./pla/build"
-    scale: 1
-    series: kubernetes
-    annotations:
-      gui-x: 500
-      gui-y: -200
-  prometheus:
-    charm: "./prometheus/build"
-    channel: "stable"
-    scale: 1
-    series: kubernetes
-    storage:
-      data: 50M
-    options:
-      default-target: "mon:8000"
-    annotations:
-      gui-x: 250
-      gui-y: 300
-  grafana:
-    charm: "./grafana/build"
-    channel: "stable"
-    scale: 1
-    series: kubernetes
-    annotations:
-      gui-x: 250
-      gui-y: 550
-  keystone:
-    charm: "./keystone/build"
-    scale: 1
-    series: kubernetes
-    annotations:
-      gui-x: -250
-      gui-y: 550
-relations:
-  - - grafana:prometheus
-    - prometheus:prometheus
-  - - kafka-k8s:zookeeper
-    - zookeeper-k8s:zookeeper
-  - - keystone:db
-    - mariadb-k8s:mysql
-  - - lcm:kafka
-    - kafka-k8s:kafka
-  - - lcm:mongodb
-    - mongodb-k8s:mongo
-  - - ro:ro
-    - lcm:ro
-  - - ro:kafka
-    - kafka-k8s:kafka
-  - - ro:mongodb
-    - mongodb-k8s:mongo
-  - - pol:kafka
-    - kafka-k8s:kafka
-  - - pol:mongodb
-    - mongodb-k8s:mongo
-  - - mon:mongodb
-    - mongodb-k8s:mongo
-  - - mon:kafka
-    - kafka-k8s:kafka
-  - - pla:kafka
-    - kafka-k8s:kafka
-  - - pla:mongodb
-    - mongodb-k8s:mongo
-  - - nbi:mongodb
-    - mongodb-k8s:mongo
-  - - nbi:kafka
-    - kafka-k8s:kafka
-  - - nbi:prometheus
-    - prometheus:prometheus
-  - - nbi:keystone
-    - keystone:keystone
-  - - mon:prometheus
-    - prometheus:prometheus
-  - - ng-ui:nbi
-    - nbi:nbi
diff --git a/installers/charm/local_osm_ha_bundle.yaml b/installers/charm/local_osm_ha_bundle.yaml
deleted file mode 100644 (file)
index 79950ca..0000000
+++ /dev/null
@@ -1,216 +0,0 @@
-# Copyright 2020 Canonical Ltd.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-#     Unless required by applicable law or agreed to in writing, software
-#     distributed under the License is distributed on an "AS IS" BASIS,
-#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#     See the License for the specific language governing permissions and
-#     limitations under the License.
-name: osm-ha
-bundle: kubernetes
-description: Local bundle for development (HA)
-applications:
-  zookeeper:
-    charm: zookeeper-k8s
-    channel: latest/edge
-    scale: 3
-    storage:
-      data: 100M
-    annotations:
-      gui-x: 0
-      gui-y: 500
-  mariadb:
-    charm: charmed-osm-mariadb-k8s
-    scale: 3
-    series: kubernetes
-    storage:
-      database: 300M
-    options:
-      password: manopw
-      root_password: osm4u
-      user: mano
-      ha-mode: true
-    annotations:
-      gui-x: -300
-      gui-y: -250
-  kafka:
-    charm: kafka-k8s
-    channel: latest/edge
-    scale: 3
-    trust: true
-    storage:
-      data: 100M
-    annotations:
-      gui-x: 0
-      gui-y: 250
-  mongodb:
-    charm: mongodb-k8s
-    channel: latest/stable
-    scale: 3
-    series: kubernetes
-    storage:
-      db: 50M
-    annotations:
-      gui-x: 0
-      gui-y: 0
-  nbi:
-    charm: ./nbi/osm-nbi.charm
-    scale: 3
-    resources:
-      image: opensourcemano/nbi:testing-daily
-    series: kubernetes
-    options:
-      database_commonkey: osm
-      auth_backend: keystone
-      log_level: DEBUG
-    annotations:
-      gui-x: 0
-      gui-y: -250
-  ro:
-    charm: ./ro/osm-ro.charm
-    scale: 3
-    resources:
-      image: opensourcemano/ro:testing-daily
-    series: kubernetes
-    options:
-      log_level: DEBUG
-    annotations:
-      gui-x: -300
-      gui-y: 250
-  ng-ui:
-    charm: ./ng-ui/osm-ng-ui.charm
-    scale: 3
-    resources:
-      image: opensourcemano/ng-ui:testing-daily
-    series: kubernetes
-    annotations:
-      gui-x: 600
-      gui-y: 0
-  lcm:
-    charm: ./lcm/osm-lcm.charm
-    scale: 3
-    resources:
-      image: opensourcemano/lcm:testing-daily
-    series: kubernetes
-    options:
-      database_commonkey: osm
-      log_level: DEBUG
-    annotations:
-      gui-x: -300
-      gui-y: 0
-  mon:
-    charm: ./mon/osm-mon.charm
-    scale: 3
-    resources:
-      image: opensourcemano/mon:testing-daily
-    series: kubernetes
-    options:
-      database_commonkey: osm
-      log_level: DEBUG
-      keystone_enabled: true
-    annotations:
-      gui-x: 300
-      gui-y: 0
-  pol:
-    charm: ./pol/osm-pol.charm
-    scale: 3
-    resources:
-      image: opensourcemano/pol:testing-daily
-    series: kubernetes
-    options:
-      log_level: DEBUG
-    annotations:
-      gui-x: -300
-      gui-y: 500
-  pla:
-    charm: ./pla/osm-pla.charm
-    scale: 3
-    resources:
-      image: opensourcemano/pla:testing-daily
-    series: kubernetes
-    options:
-      log_level: DEBUG
-    annotations:
-      gui-x: 600
-      gui-y: -250
-  prometheus:
-    charm: osm-prometheus
-    channel: latest/edge
-    scale: 3
-    series: kubernetes
-    storage:
-      data: 50M
-    options:
-      default-target: "mon:8000"
-    annotations:
-      gui-x: 300
-      gui-y: 250
-  grafana:
-    charm: osm-grafana
-    channel: latest/edge
-    scale: 3
-    series: kubernetes
-    annotations:
-      gui-x: 300
-      gui-y: 500
-  keystone:
-    charm: osm-keystone
-    channel: latest/edge
-    resources:
-      keystone-image: opensourcemano/keystone:testing-daily
-    scale: 1
-    annotations:
-      gui-x: 300
-      gui-y: -250
-relations:
-  - - grafana:prometheus
-    - prometheus:prometheus
-  - - kafka:zookeeper
-    - zookeeper:zookeeper
-  - - keystone:db
-    - mariadb:mysql
-  - - lcm:kafka
-    - kafka:kafka
-  - - lcm:mongodb
-    - mongodb:database
-  - - ro:ro
-    - lcm:ro
-  - - ro:kafka
-    - kafka:kafka
-  - - ro:mongodb
-    - mongodb:database
-  - - pol:kafka
-    - kafka:kafka
-  - - pol:mongodb
-    - mongodb:database
-  - - mon:mongodb
-    - mongodb:database
-  - - mon:kafka
-    - kafka:kafka
-  - - pla:kafka
-    - kafka:kafka
-  - - pla:mongodb
-    - mongodb:database
-  - - nbi:mongodb
-    - mongodb:database
-  - - nbi:kafka
-    - kafka:kafka
-  - - nbi:prometheus
-    - prometheus:prometheus
-  - - nbi:keystone
-    - keystone:keystone
-  - - mon:prometheus
-    - prometheus:prometheus
-  - - ng-ui:nbi
-    - nbi:nbi
-  - - mon:keystone
-    - keystone:keystone
-  - - mariadb:mysql
-    - pol:mysql
-  - - grafana:db
-    - mariadb:mysql
diff --git a/installers/charm/mariadb-k8s/.gitignore b/installers/charm/mariadb-k8s/.gitignore
deleted file mode 100644 (file)
index 712eb96..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-release/
-__pycache__
-.tox
diff --git a/installers/charm/mariadb-k8s/.yamllint.yaml b/installers/charm/mariadb-k8s/.yamllint.yaml
deleted file mode 100644 (file)
index 567eb5f..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
----
-
-extends: default
-
-yaml-files:
-  - '*.yaml'
-  - '*.yml'
-  - '.yamllint'
-ignore: |
- reactive/
- .tox
- release/
diff --git a/installers/charm/mariadb-k8s/README.md b/installers/charm/mariadb-k8s/README.md
deleted file mode 100755 (executable)
index 5c89de1..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-<!-- 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.
-
-For those usages not covered by the Apache License, Version 2.0 please
-contact: legal@canonical.com
-
-To get in touch with the maintainers, please contact:
-osm-charmers@lists.launchpad.net -->
-
-# MariaDB Operator
-
-A Juju charm deploying and managing MariaDB on Kubernetes.
-
-## Overview
-
-MariaDB turns data into structured information in a wide array of
-applications, ranging from banking to websites. Originally designed as
-enhanced, drop-in replacement for MySQL, MariaDB is used because it is fast,
-scalable and robust, with a rich ecosystem of storage engines, plugins and
-many other tools make it very versatile for a wide variety of use cases.
-
-MariaDB is developed as open source software and as a relational database it
-provides an SQL interface for accessing data. The latest versions of MariaDB
-also include GIS and JSON features.
-
-More information can be found in [the MariaDB Knowledge Base](https://mariadb.com/kb/en/documentation/).
-
-## Usage
-
-For details on using Kubernetes with Juju [see here](https://juju.is/docs/kubernetes), and for
-details on using Juju with MicroK8s for easy local testing [see here](https://juju.is/docs/microk8s-cloud).
-
-To deploy the charm into a Juju Kubernetes model:
-
-    juju deploy cs:~charmed-osm/mariadb
-
-The charm can then be easily related to an application that supports the mysql
-relation, such as:
-
-    juju deploy cs:~charmed-osm/keystone
-    juju relate keystone mariadb-k8s
-
-Once the "Workload" status of both mariadb-k8s and keystone is "active", using
-the "Application" IP of keystone (from `juju status`):
-
-    # Change as appropriate for you juju model
-    KEYSTONE_APPLICATION_IP=10.152.183.222
-    curl -i -H "Content-Type: application/json" -d '
-    { "auth": {
-        "identity": {
-          "methods": ["password"],
-          "password": {
-            "user": {
-              "name": "admin",
-              "domain": { "id": "default" },
-             "password": "admin"
-           }
-         }
-       }
-     }
-    ' "http://${KEYSTONE_APPLICATION_IP}:5000/v3/auth/tokens" ; echo
-
-This will create a token that you could use to query Keystone.
-
----
-
-For more details, [see here](https://charmhub.io/mariadb/docs/).
diff --git a/installers/charm/mariadb-k8s/actions.yaml b/installers/charm/mariadb-k8s/actions.yaml
deleted file mode 100644 (file)
index 0b33b6a..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-backup:
-  description: "Perform a backup"
-  params:
-    path:
-      description: "Path for the backup inside the unit"
-      type: string
-      default: "/var/lib/mysql"
-restore:
-  description: "Restore from a backup"
-  params:
-    path:
-      description: "Path for the backup inside the unit"
-      type: string
-      default: "/var/lib/mysql"
-remove-backup:
-  description: "Remove backup from unit"
-  params:
-    path:
-      description: "Path for the backup inside the unit"
-      type: string
-      default: "/var/lib/mysql"
diff --git a/installers/charm/mariadb-k8s/actions/backup b/installers/charm/mariadb-k8s/actions/backup
deleted file mode 100755 (executable)
index 7bfb5e4..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/bin/bash
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-DB_BACKUP_PATH=`action-get path`
-mkdir -p $DB_BACKUP_PATH
-ROOT_PASSWORD=`config-get root_password`
-mysqldump -u root -p$ROOT_PASSWORD --single-transaction --all-databases | gzip > $DB_BACKUP_PATH/backup.sql.gz || action-fail "Backup failed"
-action-set copy.cmd="kubectl cp $JUJU_MODEL_NAME/$HOSTNAME:$DB_BACKUP_PATH/backup.sql.gz backup.sql.gz"
-action-set restore.cmd="kubectl cp backup.sql.gz $JUJU_MODEL_NAME/$HOSTNAME:$DB_BACKUP_PATH/backup.sql.gz"
-action-set restore.juju="juju run-action $JUJU_UNIT_NAME restore --wait"
-
diff --git a/installers/charm/mariadb-k8s/actions/remove-backup b/installers/charm/mariadb-k8s/actions/remove-backup
deleted file mode 100755 (executable)
index f304333..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-#!/bin/bash
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-DB_BACKUP_PATH=`action-get path`
-rm $DB_BACKUP_PATH/backup.sql.gz || exit
-echo Backup successfully removed!
diff --git a/installers/charm/mariadb-k8s/actions/restore b/installers/charm/mariadb-k8s/actions/restore
deleted file mode 100755 (executable)
index 768e68e..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/bin/bash
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-DB_BACKUP_PATH=`action-get path`
-ROOT_PASSWORD=`config-get root_password`
-gunzip -c $DB_BACKUP_PATH/backup.sql.gz | mysql -uroot -p$ROOT_PASSWORD || action-fail "Restore failed"
-action-set message="Backup restored successfully"
\ No newline at end of file
diff --git a/installers/charm/mariadb-k8s/charmcraft.yaml b/installers/charm/mariadb-k8s/charmcraft.yaml
deleted file mode 100644 (file)
index 69a510c..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-type: "charm"
-bases:
-  - build-on:
-      - name: "ubuntu"
-        channel: "20.04"
-    run-on:
-      - name: "ubuntu"
-        channel: "20.04"
-parts:
-  charm:
-    source: .
-    plugin: reactive
-    build-snaps: [charm]
diff --git a/installers/charm/mariadb-k8s/config.yaml b/installers/charm/mariadb-k8s/config.yaml
deleted file mode 100755 (executable)
index 8a606a4..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-options:
-  user:
-    type: string
-    description: 'The database user name.'
-    default: 'mysql'
-  password:
-    type: string
-    description: 'The database user password.'
-    default: 'password'
-  database:
-    type: string
-    description: 'The database name.'
-    default: 'database'
-  root_password:
-    type: string
-    description: 'The database root password.'
-    default: 'root'
-  mysql_port:
-    type: string
-    description: 'The mysql port'
-    default: '3306'
-  query-cache-type:
-    default: "OFF"
-    type: string
-    description: "Query cache is usually a good idea, \
-      but can hurt concurrency. \
-      Valid values are \"OFF\", \"ON\", or \"DEMAND\"."
-  query-cache-size:
-    default: !!int "0"
-    type: int
-    description: "Override the computed version from dataset-size. \
-      Still works if query-cache-type is \"OFF\" since sessions \
-      can override the cache type setting on their own."
-  ha-mode:
-    type: boolean
-    description: Indicates if the charm should have the capabilities to scale
-    default: false
-  image:
-    type: string
-    description: OCI image
-    default: rocks.canonical.com:443/mariadb/server:10.3
-  ha-image:
-    type: string
-    description: OCI image
-    default: rocks.canonical.com:443/canonicalosm/galera-mysql:latest
diff --git a/installers/charm/mariadb-k8s/icon.svg b/installers/charm/mariadb-k8s/icon.svg
deleted file mode 100644 (file)
index 69b42ee..0000000
+++ /dev/null
@@ -1,345 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:xlink="http://www.w3.org/1999/xlink"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   version="1.1"
-   id="svg3767"
-   width="640"
-   height="578"
-   viewBox="0 0 640 578"
-   sodipodi:docname="icon.svg"
-   inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
-  <metadata
-     id="metadata3773">
-    <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title></dc:title>
-      </cc:Work>
-    </rdf:RDF>
-  </metadata>
-  <defs
-     id="defs3771" />
-  <sodipodi:namedview
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1"
-     objecttolerance="10"
-     gridtolerance="10"
-     guidetolerance="10"
-     inkscape:pageopacity="0"
-     inkscape:pageshadow="2"
-     inkscape:window-width="2560"
-     inkscape:window-height="1376"
-     id="namedview3769"
-     showgrid="false"
-     inkscape:zoom="2.100346"
-     inkscape:cx="320"
-     inkscape:cy="289"
-     inkscape:window-x="0"
-     inkscape:window-y="27"
-     inkscape:window-maximized="1"
-     inkscape:current-layer="svg3767" />
-  <image
-     width="640"
-     height="578"
-     preserveAspectRatio="none"
-     xlink:href="data:image/png;base64,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
-PAAAAABJRU5ErkJggg==
-"
-     id="image3775"
-     x="0"
-     y="0" />
-</svg>
diff --git a/installers/charm/mariadb-k8s/layer.yaml b/installers/charm/mariadb-k8s/layer.yaml
deleted file mode 100644 (file)
index f9b5dd9..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-includes:
-  - "layer:caas-base"
-  - 'layer:status'
-  - 'layer:leadership'
-  - "layer:osm-common"
-  - 'interface:juju-relation-mysql'
-
-repo: https://github.com/wallyworld/caas.git
diff --git a/installers/charm/mariadb-k8s/metadata.yaml b/installers/charm/mariadb-k8s/metadata.yaml
deleted file mode 100755 (executable)
index a802115..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-name: osm-mariadb
-summary: MariaDB is a popular database server made by the developers of MySQL.
-# docs: https://discourse.charmhub.io/t/mariadb-documentation-overview/4116
-maintainers:
-  - OSM Charmers <osm-charmers@lists.launchpad.net>
-description: |
-  MariaDB Server is one of the most popular database servers in the world.
-  It's made by the original developers of MySQL and guaranteed to stay open
-  source. Notable users include Wikipedia, WordPress.com and Google.
-  https://mariadb.org/
-tags:
-  - database
-  - openstack
-provides:
-  mysql:
-    interface: mysql
-series:
-  - kubernetes
-storage:
-  database:
-    type: filesystem
-    location: /var/lib/mysql
-deployment:
-  type: stateful
-  service: cluster
diff --git a/installers/charm/mariadb-k8s/reactive/osm_mariadb.py b/installers/charm/mariadb-k8s/reactive/osm_mariadb.py
deleted file mode 100644 (file)
index 4eedcfb..0000000
+++ /dev/null
@@ -1,141 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-from charms.layer.caas_base import pod_spec_set
-from charms.reactive import when, when_not, hook
-from charms.reactive import endpoint_from_flag
-from charms.reactive.flags import set_flag, get_state, clear_flag
-
-from charmhelpers.core.hookenv import (
-    log,
-    metadata,
-    config,
-    application_name,
-)
-from charms import layer
-from charms.osm.k8s import is_pod_up, get_service_ip
-
-
-@hook("upgrade-charm")
-@when("leadership.is_leader")
-def upgrade():
-    clear_flag("mariadb-k8s.configured")
-
-
-@when("config.changed")
-@when("leadership.is_leader")
-def restart():
-    clear_flag("mariadb-k8s.configured")
-
-
-@when_not("mariadb-k8s.configured")
-@when("leadership.is_leader")
-def configure():
-    layer.status.maintenance("Configuring mariadb-k8s container")
-
-    spec = make_pod_spec()
-    log("set pod spec:\n{}".format(spec))
-    pod_spec_set(spec)
-
-    set_flag("mariadb-k8s.configured")
-
-
-@when("mariadb-k8s.configured")
-def set_mariadb_active():
-    layer.status.active("ready")
-
-
-@when_not("leadership.is_leader")
-def non_leaders_active():
-    layer.status.active("ready")
-
-
-@when("mariadb-k8s.configured", "mysql.database.requested")
-def provide_database():
-    mysql = endpoint_from_flag("mysql.database.requested")
-
-    if not is_pod_up("mysql"):
-        log("The pod is not ready.")
-        return
-
-    for request, application in mysql.database_requests().items():
-        try:
-
-            log("request -> {0} for app -> {1}".format(request, application))
-            user = get_state("user")
-            password = get_state("password")
-            database_name = get_state("database")
-            root_password = get_state("root_password")
-
-            log("db params: {0}:{1}@{2}".format(user, password, database_name))
-
-            service_ip = get_service_ip("mysql")
-            if service_ip:
-                mysql.provide_database(
-                    request_id=request,
-                    host=service_ip,
-                    port=3306,
-                    database_name=database_name,
-                    user=user,
-                    password=password,
-                    root_password=root_password,
-                )
-                mysql.mark_complete()
-        except Exception as e:
-            log("Exception while providing database: {}".format(e))
-
-
-def make_pod_spec():
-    """Make pod specification for Kubernetes
-
-    Returns:
-        pod_spec: Pod specification for Kubernetes
-    """
-    if config().get("ha-mode"):
-        with open("reactive/spec_template_ha.yaml") as spec_file:
-            pod_spec_template = spec_file.read()
-        image = config().get("ha-image")
-    else:
-        with open("reactive/spec_template.yaml") as spec_file:
-            pod_spec_template = spec_file.read()
-        image = config().get("image")
-
-    md = metadata()
-    cfg = config()
-
-    user = cfg.get("user")
-    password = cfg.get("password")
-    database = cfg.get("database")
-    root_password = cfg.get("root_password")
-    app_name = application_name()
-
-    set_flag("user", user)
-    set_flag("password", password)
-    set_flag("database", database)
-    set_flag("root_password", root_password)
-
-    data = {
-        "name": md.get("name"),
-        "docker_image": image,
-        "application_name": app_name,
-    }
-    data.update(cfg)
-    return pod_spec_template % data
diff --git a/installers/charm/mariadb-k8s/reactive/spec_template.yaml b/installers/charm/mariadb-k8s/reactive/spec_template.yaml
deleted file mode 100644 (file)
index 0a1facc..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-version: 2
-containers:
-  - name: %(name)s
-    image: %(docker_image)s
-    ports:
-      - containerPort: %(mysql_port)s
-        protocol: TCP
-        name: main
-    config:
-      MARIADB_ROOT_PASSWORD: %(root_password)s
-      MARIADB_USER: %(user)s
-      MARIADB_PASSWORD: %(password)s
-      MARIADB_DATABASE: %(database)s
-    kubernetes:
-      readinessProbe:
-        tcpSocket:
-          port: %(mysql_port)s
-        initialDelaySeconds: 10
-        periodSeconds: 10
-        timeoutSeconds: 5
-        successThreshold: 1
-        failureThreshold: 3
-      livenessProbe:
-        tcpSocket:
-          port: %(mysql_port)s
-        initialDelaySeconds: 120
-        periodSeconds: 10
-        timeoutSeconds: 5
-        successThreshold: 1
-        failureThreshold: 3
diff --git a/installers/charm/mariadb-k8s/reactive/spec_template_ha.yaml b/installers/charm/mariadb-k8s/reactive/spec_template_ha.yaml
deleted file mode 100644 (file)
index f5ebf20..0000000
+++ /dev/null
@@ -1,97 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-version: 2
-service:
-  scalePolicy: serial
-  annotations:
-    service.alpha.kubernetes.io/tolerate-unready-endpoints: "true"
-containers:
-  - name: %(name)s
-    image: %(docker_image)s
-    kubernetes:
-      readinessProbe:
-        tcpSocket:
-          port: %(mysql_port)s
-        initialDelaySeconds: 10
-        periodSeconds: 10
-        timeoutSeconds: 5
-        successThreshold: 1
-        failureThreshold: 3
-      livenessProbe:
-        exec:
-          command: ["bash", "-c", "mysql -uroot -p\"${MYSQL_ROOT_PASSWORD}\" -e 'show databases;'"]
-        initialDelaySeconds: 120
-        periodSeconds: 10
-        timeoutSeconds: 5
-        successThreshold: 1
-        failureThreshold: 3
-    ports:
-    - containerPort: %(mysql_port)s
-      protocol: TCP
-      name: main
-    - containerPort: 4444
-      name: sst
-    - containerPort: 4567
-      name: replication
-    - containerPort: 4568
-      name: ist
-    config:
-      MYSQL_ROOT_PASSWORD: %(root_password)s
-      APPLICATION_NAME: %(application_name)s
-      MYSQL_USER: %(user)s
-      MYSQL_PASSWORD: %(password)s
-      MYSQL_DATABASE: %(database)s
-    files:
-      - name: configurations
-        mountPath: /etc/mysqlconfiguration
-        files:
-          galera.cnf: |
-            [galera]
-            user = mysql
-            bind-address = 0.0.0.0
-        
-            default_storage_engine = InnoDB
-            binlog_format = ROW
-            innodb_autoinc_lock_mode = 2
-            innodb_flush_log_at_trx_commit = 0
-            query_cache_size = 0
-            host_cache_size = 0
-            query_cache_type = 0
-
-            # MariaDB Galera settings
-            wsrep_on=ON
-            wsrep_provider=/usr/lib/galera/libgalera_smm.so
-            wsrep_sst_method=rsync
-
-            # Cluster settings (automatically updated)
-            wsrep_cluster_address=gcomm://
-            wsrep_cluster_name=vimdb_cluser
-            wsrep_node_address=127.0.0.1
-          mariadb.cnf: |
-            [client]
-            default-character-set = utf8
-            [mysqld]
-            character-set-server  = utf8
-            collation-server      = utf8_general_ci
-            plugin_load_add = feedbackx#
-            # InnoDB tuning
-            innodb_log_file_size  = 50M
diff --git a/installers/charm/mariadb-k8s/test-requirements.txt b/installers/charm/mariadb-k8s/test-requirements.txt
deleted file mode 100644 (file)
index 04f2d76..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-git+https://github.com/davigar15/zaza.git#egg=zaza
-mysql.connector
\ No newline at end of file
diff --git a/installers/charm/mariadb-k8s/tests/basic_deployment.py b/installers/charm/mariadb-k8s/tests/basic_deployment.py
deleted file mode 100644 (file)
index fd6520f..0000000
+++ /dev/null
@@ -1,136 +0,0 @@
-#!/usr/bin/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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-import unittest
-import zaza.model as model
-
-import mysql.connector as mysql
-
-# from mysql.connector import errorcode
-
-APPLICATION_NAME = "mariadb-k8s"
-UNIT_NAME = "mariadb-k8s/0"
-ROOT_USER = "root"
-ROOT_PASSWORD = "osm4u"
-USER = "mano"
-PASSWORD = "manopw"
-ACTION_SUCCESS_STATUS = "completed"
-
-
-def create_database(cnx, database_name):
-    try:
-        if not database_exists(cnx, database_name):
-            cursor = cnx.cursor()
-            cursor.execute(
-                "CREATE DATABASE {} DEFAULT CHARACTER SET 'utf8'".format(database_name)
-            )
-            return database_exists(cnx, database_name)
-        else:
-            return True
-    except mysql.Error as err:
-        print("Failed creating database {}: {}".format(database_name, err))
-
-
-def delete_database(cnx, database_name):
-    try:
-        if database_exists(cnx, database_name):
-            cursor = cnx.cursor()
-            cursor.execute("DROP DATABASE {}".format(database_name))
-            return not database_exists(cnx, database_name)
-        else:
-            return True
-    except mysql.Error as err:
-        print("Failed deleting database {}: {}".format(database_name, err))
-
-
-def database_exists(cnx, database_name):
-    try:
-        cursor = cnx.cursor()
-        cursor.execute("SHOW DATABASES")
-        databases = cursor.fetchall()
-        exists = False
-        for database in databases:
-            if database[0] == database_name:
-                exists = True
-        cursor.close()
-        return exists
-    except mysql.Error as err:
-        print("Failed deleting database {}: {}".format(database_name, err))
-        return False
-
-
-class BasicDeployment(unittest.TestCase):
-    def setUp(self):
-        super().setUp()
-        self.ip = model.get_status().applications[APPLICATION_NAME]["public-address"]
-        try:
-            self.cnx = mysql.connect(
-                user=ROOT_USER, password=ROOT_PASSWORD, host=self.ip
-            )
-        except mysql.Error as err:
-            print("Couldn't connect to mariadb-k8s : {}".format(err))
-
-    def tearDown(self):
-        super().tearDown()
-        self.cnx.close()
-
-    def test_mariadb_connection_root(self):
-        pass
-
-    def test_mariadb_connection_user(self):
-        try:
-            cnx = mysql.connect(user=USER, password=PASSWORD, host=self.ip)
-            cnx.close()
-        except mysql.Error as err:
-            print("Couldn't connect to mariadb-k8s with user creds: {}".format(err))
-
-    def test_mariadb_create_database(self):
-        created = create_database(self.cnx, "test_database")
-        self.failIf(not created)
-
-    def test_mariadb_backup_action(self, db_name="test_backup"):
-        created = create_database(self.cnx, db_name)
-        self.failIf(not created)
-        try:
-            action = model.run_action(UNIT_NAME, "backup", raise_on_failure=True)
-            self.assertEqual(action.status, ACTION_SUCCESS_STATUS)
-        except model.ActionFailed as err:
-            print("Action failed: {}".format(err))
-
-    def test_mariadb_remove_backup_action(self):
-        self.test_mariadb_backup_action(db_name="test_remove_backup")
-        try:
-            action = model.run_action(UNIT_NAME, "remove-backup", raise_on_failure=True)
-            self.assertEqual(action.status, ACTION_SUCCESS_STATUS)
-        except model.ActionFailed as err:
-            print("Action failed: {}".format(err))
-
-    def test_mariadb_restore_action(self):
-        self.test_mariadb_backup_action(db_name="test_restore")
-        deleted = delete_database(self.cnx, "test_restore")
-        self.failIf(not deleted)
-        try:
-            action = model.run_action(UNIT_NAME, "restore", raise_on_failure=True)
-            self.assertEqual(action.status, "completed")
-            self.assertTrue(database_exists(self.cnx, "test_restore"))
-        except model.ActionFailed as err:
-            print("Action failed: {}".format(err))
diff --git a/installers/charm/mariadb-k8s/tests/bundles/mariadb-ha.yaml b/installers/charm/mariadb-k8s/tests/bundles/mariadb-ha.yaml
deleted file mode 100644 (file)
index 7692bd5..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-bundle: kubernetes
-applications:
-  mariadb-k8s:
-    charm: '../../release/'
-    scale: 2
-    options:
-      password: manopw
-      root_password: osm4u
-      user: mano
-      database: database
-      mysql_port: "3306"
-      query-cache-type: "OFF"
-      query-cache-size: 0
-      ha-mode: true
-      image: 'rocks.canonical.com:443/canonicalosm/galera-mysql:latest'
-    series: kubernetes
-    storage:
-      database: 50M
diff --git a/installers/charm/mariadb-k8s/tests/bundles/mariadb.yaml b/installers/charm/mariadb-k8s/tests/bundles/mariadb.yaml
deleted file mode 100644 (file)
index e3e3aa3..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-bundle: kubernetes
-applications:
-  mariadb-k8s:
-    charm: '../../release/'
-    scale: 1
-    options:
-      password: manopw
-      root_password: osm4u
-      user: mano
-      database: database
-      mysql_port: "3306"
-      query-cache-type: "OFF"
-      query-cache-size: 0
-      ha-mode: false
-    series: kubernetes
-    storage:
-      database: 50M
diff --git a/installers/charm/mariadb-k8s/tests/tests.yaml b/installers/charm/mariadb-k8s/tests/tests.yaml
deleted file mode 100644 (file)
index df2b59c..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-gate_bundles:
-  - mariadb
-  - mariadb-ha
-smoke_bundles:
-  - mariadb
-tests:
-  - tests.basic_deployment.BasicDeployment
diff --git a/installers/charm/mariadb-k8s/tox.ini b/installers/charm/mariadb-k8s/tox.ini
deleted file mode 100644 (file)
index 28d60be..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-[tox]
-envlist = pep8
-skipsdist = True
-
-[testenv]
-setenv = VIRTUAL_ENV={envdir}
-         PYTHONHASHSEED=0
-whitelist_externals = juju
-passenv = HOME TERM CS_API_* OS_* AMULET_*
-deps = -r{toxinidir}/test-requirements.txt
-install_command =
-  pip install {opts} {packages}
-
-[testenv:build]
-basepython = python3
-passenv=HTTP_PROXY HTTPS_PROXY NO_PROXY
-setenv = CHARM_LAYERS_DIR = /tmp
-         CHARM_INTERFACES_DIR = /tmp/canonical-osm/charms/interfaces/
-whitelist_externals = git
-                      charm
-                      rm
-                      mv
-commands =
-    rm -rf /tmp/canonical-osm /tmp/osm-common
-    rm -rf release
-    git clone https://git.launchpad.net/canonical-osm /tmp/canonical-osm
-    git clone https://git.launchpad.net/charm-osm-common /tmp/osm-common
-    charm build . --build-dir /tmp
-    mv /tmp/mariadb-k8s/ release/
-
-[testenv:black]
-basepython = python3
-deps =
-    black
-    yamllint
-    flake8
-commands =
-    black --check --diff .
-    yamllint .
-    flake8 reactive/ --max-line-length=88
-    flake8 tests/ --max-line-length=88
-
-[testenv:pep8]
-basepython = python3
-deps=charm-tools
-commands = charm-proof
-
-[testenv:func-noop]
-basepython = python3
-commands =
-    true
-
-[testenv:func]
-basepython = python3
-commands = functest-run-suite
-
-
-[testenv:func-smoke]
-basepython = python3
-commands = functest-run-suite --keep-model --smoke
-
-[testenv:venv]
-commands = {posargs}
diff --git a/installers/charm/mongodb-exporter/.gitignore b/installers/charm/mongodb-exporter/.gitignore
deleted file mode 100644 (file)
index 2885df2..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-venv
-.vscode
-build
-*.charm
-.coverage
-coverage.xml
-.stestr
-cover
-release
\ No newline at end of file
diff --git a/installers/charm/mongodb-exporter/.jujuignore b/installers/charm/mongodb-exporter/.jujuignore
deleted file mode 100644 (file)
index 3ae3e7d..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-venv
-.vscode
-build
-*.charm
-.coverage
-coverage.xml
-.gitignore
-.stestr
-cover
-release
-tests/
-requirements*
-tox.ini
diff --git a/installers/charm/mongodb-exporter/.yamllint.yaml b/installers/charm/mongodb-exporter/.yamllint.yaml
deleted file mode 100644 (file)
index d71fb69..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
----
-extends: default
-
-yaml-files:
-  - "*.yaml"
-  - "*.yml"
-  - ".yamllint"
-ignore: |
-  .tox
-  cover/
-  build/
-  venv
-  release/
diff --git a/installers/charm/mongodb-exporter/README.md b/installers/charm/mongodb-exporter/README.md
deleted file mode 100644 (file)
index 84df4c9..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<!-- 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.
-
-For those usages not covered by the Apache License, Version 2.0 please
-contact: legal@canonical.com
-
-To get in touch with the maintainers, please contact:
-osm-charmers@lists.launchpad.net -->
-
-# Prometheus Mongodb Exporter operator Charm for Kubernetes
-
-## Requirements
diff --git a/installers/charm/mongodb-exporter/charmcraft.yaml b/installers/charm/mongodb-exporter/charmcraft.yaml
deleted file mode 100644 (file)
index 0a285a9..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-type: charm
-bases:
-  - build-on:
-      - name: ubuntu
-        channel: "20.04"
-        architectures: ["amd64"]
-    run-on:
-      - name: ubuntu
-        channel: "20.04"
-        architectures:
-          - amd64
-          - aarch64
-          - arm64
-parts:
-  charm:
-    build-packages: [git]
diff --git a/installers/charm/mongodb-exporter/config.yaml b/installers/charm/mongodb-exporter/config.yaml
deleted file mode 100644 (file)
index fe5cd63..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-options:
-  ingress_class:
-    type: string
-    description: |
-      Ingress class name. This is useful for selecting the ingress to be used
-      in case there are multiple ingresses in the underlying k8s clusters.
-  ingress_whitelist_source_range:
-    type: string
-    description: |
-      A comma-separated list of CIDRs to store in the
-      ingress.kubernetes.io/whitelist-source-range annotation.
-
-      This can be used to lock down access to
-      Keystone based on source IP address.
-    default: ""
-  tls_secret_name:
-    type: string
-    description: TLS Secret name
-    default: ""
-  site_url:
-    type: string
-    description: Ingress URL
-    default: ""
-  cluster_issuer:
-    type: string
-    description: Name of the cluster issuer for TLS certificates
-    default: ""
-  mongodb_uri:
-    type: string
-    description: MongoDB URI (external database)
-  image_pull_policy:
-    type: string
-    description: |
-      ImagePullPolicy configuration for the pod.
-      Possible values: always, ifnotpresent, never
-    default: always
-  security_context:
-    description: Enables the security context of the pods
-    type: boolean
-    default: false
diff --git a/installers/charm/mongodb-exporter/metadata.yaml b/installers/charm/mongodb-exporter/metadata.yaml
deleted file mode 100644 (file)
index c3a0b77..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-name: mongodb-exporter-k8s
-summary: OSM Prometheus Mongodb Exporter
-description: |
-  A CAAS charm to deploy OSM's Prometheus Mongodb Exporter.
-series:
-  - kubernetes
-tags:
-  - kubernetes
-  - osm
-  - prometheus
-  - mongodb-exporter
-min-juju-version: 2.8.0
-deployment:
-  type: stateless
-  service: cluster
-resources:
-  image:
-    type: oci-image
-    description: Image of mongodb-exporter
-    upstream-source: "bitnami/mongodb-exporter:0.30.0"
-provides:
-  prometheus-scrape:
-    interface: prometheus
-  grafana-dashboard:
-    interface: grafana-dashboard
-requires:
-  mongodb:
-    interface: mongodb
diff --git a/installers/charm/mongodb-exporter/requirements-test.txt b/installers/charm/mongodb-exporter/requirements-test.txt
deleted file mode 100644 (file)
index 316f6d2..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-
-mock==4.0.3
diff --git a/installers/charm/mongodb-exporter/requirements.txt b/installers/charm/mongodb-exporter/requirements.txt
deleted file mode 100644 (file)
index 8bb93ad..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-git+https://github.com/charmed-osm/ops-lib-charmed-osm/@master
diff --git a/installers/charm/mongodb-exporter/src/charm.py b/installers/charm/mongodb-exporter/src/charm.py
deleted file mode 100755 (executable)
index 0ee127c..0000000
+++ /dev/null
@@ -1,275 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-# pylint: disable=E0213
-
-from ipaddress import ip_network
-import logging
-from pathlib import Path
-from typing import NoReturn, Optional
-from urllib.parse import urlparse
-
-from ops.main import main
-from opslib.osm.charm import CharmedOsmBase, RelationsMissing
-from opslib.osm.interfaces.grafana import GrafanaDashboardTarget
-from opslib.osm.interfaces.mongo import MongoClient
-from opslib.osm.interfaces.prometheus import PrometheusScrapeTarget
-from opslib.osm.pod import (
-    ContainerV3Builder,
-    IngressResourceV3Builder,
-    PodRestartPolicy,
-    PodSpecV3Builder,
-)
-from opslib.osm.validator import ModelValidator, validator
-
-
-logger = logging.getLogger(__name__)
-
-PORT = 9216
-
-
-class ConfigModel(ModelValidator):
-    site_url: Optional[str]
-    cluster_issuer: Optional[str]
-    ingress_class: Optional[str]
-    ingress_whitelist_source_range: Optional[str]
-    tls_secret_name: Optional[str]
-    mongodb_uri: Optional[str]
-    image_pull_policy: str
-    security_context: bool
-
-    @validator("site_url")
-    def validate_site_url(cls, v):
-        if v:
-            parsed = urlparse(v)
-            if not parsed.scheme.startswith("http"):
-                raise ValueError("value must start with http")
-        return v
-
-    @validator("ingress_whitelist_source_range")
-    def validate_ingress_whitelist_source_range(cls, v):
-        if v:
-            ip_network(v)
-        return v
-
-    @validator("mongodb_uri")
-    def validate_mongodb_uri(cls, v):
-        if v and not v.startswith("mongodb://"):
-            raise ValueError("mongodb_uri is not properly formed")
-        return v
-
-    @validator("image_pull_policy")
-    def validate_image_pull_policy(cls, v):
-        values = {
-            "always": "Always",
-            "ifnotpresent": "IfNotPresent",
-            "never": "Never",
-        }
-        v = v.lower()
-        if v not in values.keys():
-            raise ValueError("value must be always, ifnotpresent or never")
-        return values[v]
-
-
-class MongodbExporterCharm(CharmedOsmBase):
-    def __init__(self, *args) -> NoReturn:
-        super().__init__(*args, oci_image="image")
-
-        # Provision Kafka relation to exchange information
-        self.mongodb_client = MongoClient(self, "mongodb")
-        self.framework.observe(self.on["mongodb"].relation_changed, self.configure_pod)
-        self.framework.observe(self.on["mongodb"].relation_broken, self.configure_pod)
-
-        # Register relation to provide a Scraping Target
-        self.scrape_target = PrometheusScrapeTarget(self, "prometheus-scrape")
-        self.framework.observe(
-            self.on["prometheus-scrape"].relation_joined, self._publish_scrape_info
-        )
-
-        # Register relation to provide a Dasboard Target
-        self.dashboard_target = GrafanaDashboardTarget(self, "grafana-dashboard")
-        self.framework.observe(
-            self.on["grafana-dashboard"].relation_joined, self._publish_dashboard_info
-        )
-
-    def _publish_scrape_info(self, event) -> NoReturn:
-        """Publishes scraping information for Prometheus.
-
-        Args:
-            event (EventBase): Prometheus relation event.
-        """
-        if self.unit.is_leader():
-            hostname = (
-                urlparse(self.model.config["site_url"]).hostname
-                if self.model.config["site_url"]
-                else self.model.app.name
-            )
-            port = str(PORT)
-            if self.model.config.get("site_url", "").startswith("https://"):
-                port = "443"
-            elif self.model.config.get("site_url", "").startswith("http://"):
-                port = "80"
-
-            self.scrape_target.publish_info(
-                hostname=hostname,
-                port=port,
-                metrics_path="/metrics",
-                scrape_interval="30s",
-                scrape_timeout="15s",
-            )
-
-    def _publish_dashboard_info(self, event) -> NoReturn:
-        """Publish dashboards for Grafana.
-
-        Args:
-            event (EventBase): Grafana relation event.
-        """
-        if self.unit.is_leader():
-            self.dashboard_target.publish_info(
-                name="osm-mongodb",
-                dashboard=Path("templates/mongodb_exporter_dashboard.json").read_text(),
-            )
-
-    def _check_missing_dependencies(self, config: ConfigModel):
-        """Check if there is any relation missing.
-
-        Args:
-            config (ConfigModel): object with configuration information.
-
-        Raises:
-            RelationsMissing: if kafka is missing.
-        """
-        missing_relations = []
-
-        if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit():
-            missing_relations.append("mongodb")
-
-        if missing_relations:
-            raise RelationsMissing(missing_relations)
-
-    def build_pod_spec(self, image_info):
-        """Build the PodSpec to be used.
-
-        Args:
-            image_info (str): container image information.
-
-        Returns:
-            Dict: PodSpec information.
-        """
-        # Validate config
-        config = ConfigModel(**dict(self.config))
-
-        if config.mongodb_uri and not self.mongodb_client.is_missing_data_in_unit():
-            raise Exception("Mongodb data cannot be provided via config and relation")
-
-        # Check relations
-        self._check_missing_dependencies(config)
-
-        unparsed = (
-            config.mongodb_uri
-            if config.mongodb_uri
-            else self.mongodb_client.connection_string
-        )
-        parsed = urlparse(unparsed)
-        mongodb_uri = f"mongodb://{parsed.netloc.split(',')[0]}{parsed.path}"
-        if parsed.query:
-            mongodb_uri += f"?{parsed.query}"
-
-        # Create Builder for the PodSpec
-        pod_spec_builder = PodSpecV3Builder(
-            enable_security_context=config.security_context
-        )
-
-        # Add secrets to the pod
-        mongodb_secret_name = f"{self.app.name}-mongodb-secret"
-        pod_spec_builder.add_secret(mongodb_secret_name, {"uri": mongodb_uri})
-
-        # Build container
-        container_builder = ContainerV3Builder(
-            self.app.name,
-            image_info,
-            config.image_pull_policy,
-            run_as_non_root=config.security_context,
-        )
-        container_builder.add_port(name="exporter", port=PORT)
-        container_builder.add_http_readiness_probe(
-            path="/api/health",
-            port=PORT,
-            initial_delay_seconds=10,
-            period_seconds=10,
-            timeout_seconds=5,
-            success_threshold=1,
-            failure_threshold=3,
-        )
-        container_builder.add_http_liveness_probe(
-            path="/api/health",
-            port=PORT,
-            initial_delay_seconds=60,
-            timeout_seconds=30,
-            failure_threshold=10,
-        )
-
-        container_builder.add_secret_envs(mongodb_secret_name, {"MONGODB_URI": "uri"})
-        container = container_builder.build()
-
-        # Add container to PodSpec
-        pod_spec_builder.add_container(container)
-
-        # Add Pod restart policy
-        restart_policy = PodRestartPolicy()
-        restart_policy.add_secrets(secret_names=(mongodb_secret_name,))
-        pod_spec_builder.set_restart_policy(restart_policy)
-
-        # Add ingress resources to PodSpec if site url exists
-        if config.site_url:
-            parsed = urlparse(config.site_url)
-            annotations = {}
-            if config.ingress_class:
-                annotations["kubernetes.io/ingress.class"] = config.ingress_class
-            ingress_resource_builder = IngressResourceV3Builder(
-                f"{self.app.name}-ingress", annotations
-            )
-
-            if config.ingress_whitelist_source_range:
-                annotations[
-                    "nginx.ingress.kubernetes.io/whitelist-source-range"
-                ] = config.ingress_whitelist_source_range
-
-            if config.cluster_issuer:
-                annotations["cert-manager.io/cluster-issuer"] = config.cluster_issuer
-
-            if parsed.scheme == "https":
-                ingress_resource_builder.add_tls(
-                    [parsed.hostname], config.tls_secret_name
-                )
-            else:
-                annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
-
-            ingress_resource_builder.add_rule(parsed.hostname, self.app.name, PORT)
-            ingress_resource = ingress_resource_builder.build()
-            pod_spec_builder.add_ingress_resource(ingress_resource)
-
-        return pod_spec_builder.build()
-
-
-if __name__ == "__main__":
-    main(MongodbExporterCharm)
diff --git a/installers/charm/mongodb-exporter/src/pod_spec.py b/installers/charm/mongodb-exporter/src/pod_spec.py
deleted file mode 100644 (file)
index ff42e02..0000000
+++ /dev/null
@@ -1,305 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-from ipaddress import ip_network
-import logging
-from typing import Any, Dict, List
-from urllib.parse import urlparse
-
-logger = logging.getLogger(__name__)
-
-
-def _validate_ip_network(network: str) -> bool:
-    """Validate IP network.
-
-    Args:
-        network (str): IP network range.
-
-    Returns:
-        bool: True if valid, false otherwise.
-    """
-    if not network:
-        return True
-
-    try:
-        ip_network(network)
-    except ValueError:
-        return False
-
-    return True
-
-
-def _validate_data(config_data: Dict[str, Any], relation_data: Dict[str, Any]) -> bool:
-    """Validates passed information.
-
-    Args:
-        config_data (Dict[str, Any]): configuration information.
-        relation_data (Dict[str, Any]): relation information
-
-    Raises:
-        ValueError: when config and/or relation data is not valid.
-    """
-    config_validators = {
-        "site_url": lambda value, _: isinstance(value, str)
-        if value is not None
-        else True,
-        "cluster_issuer": lambda value, _: isinstance(value, str)
-        if value is not None
-        else True,
-        "ingress_whitelist_source_range": lambda value, _: _validate_ip_network(value),
-        "tls_secret_name": lambda value, _: isinstance(value, str)
-        if value is not None
-        else True,
-    }
-    relation_validators = {
-        "mongodb_connection_string": lambda value, _: (
-            isinstance(value, str) and value.startswith("mongodb://")
-        )
-    }
-    problems = []
-
-    for key, validator in config_validators.items():
-        valid = validator(config_data.get(key), config_data)
-
-        if not valid:
-            problems.append(key)
-
-    for key, validator in relation_validators.items():
-        valid = validator(relation_data.get(key), relation_data)
-
-        if not valid:
-            problems.append(key)
-
-    if len(problems) > 0:
-        raise ValueError("Errors found in: {}".format(", ".join(problems)))
-
-    return True
-
-
-def _make_pod_ports(port: int) -> List[Dict[str, Any]]:
-    """Generate pod ports details.
-
-    Args:
-        port (int): port to expose.
-
-    Returns:
-        List[Dict[str, Any]]: pod port details.
-    """
-    return [
-        {
-            "name": "mongo-exporter",
-            "containerPort": port,
-            "protocol": "TCP",
-        }
-    ]
-
-
-def _make_pod_envconfig(
-    config: Dict[str, Any], relation_state: Dict[str, Any]
-) -> Dict[str, Any]:
-    """Generate pod environment configuration.
-
-    Args:
-        config (Dict[str, Any]): configuration information.
-        relation_state (Dict[str, Any]): relation state information.
-
-    Returns:
-        Dict[str, Any]: pod environment configuration.
-    """
-    parsed = urlparse(relation_state.get("mongodb_connection_string"))
-
-    envconfig = {
-        "MONGODB_URI": f"mongodb://{parsed.netloc.split(',')[0]}{parsed.path}",
-    }
-
-    if parsed.query:
-        envconfig["MONGODB_URI"] += f"?{parsed.query}"
-
-    return envconfig
-
-
-def _make_pod_ingress_resources(
-    config: Dict[str, Any], app_name: str, port: int
-) -> List[Dict[str, Any]]:
-    """Generate pod ingress resources.
-
-    Args:
-        config (Dict[str, Any]): configuration information.
-        app_name (str): application name.
-        port (int): port to expose.
-
-    Returns:
-        List[Dict[str, Any]]: pod ingress resources.
-    """
-    site_url = config.get("site_url")
-
-    if not site_url:
-        return
-
-    parsed = urlparse(site_url)
-
-    if not parsed.scheme.startswith("http"):
-        return
-
-    ingress_whitelist_source_range = config["ingress_whitelist_source_range"]
-    cluster_issuer = config["cluster_issuer"]
-
-    annotations = {}
-
-    if ingress_whitelist_source_range:
-        annotations[
-            "nginx.ingress.kubernetes.io/whitelist-source-range"
-        ] = ingress_whitelist_source_range
-
-    if cluster_issuer:
-        annotations["cert-manager.io/cluster-issuer"] = cluster_issuer
-
-    ingress_spec_tls = None
-
-    if parsed.scheme == "https":
-        ingress_spec_tls = [{"hosts": [parsed.hostname]}]
-        tls_secret_name = config["tls_secret_name"]
-        if tls_secret_name:
-            ingress_spec_tls[0]["secretName"] = tls_secret_name
-    else:
-        annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
-
-    ingress = {
-        "name": "{}-ingress".format(app_name),
-        "annotations": annotations,
-        "spec": {
-            "rules": [
-                {
-                    "host": parsed.hostname,
-                    "http": {
-                        "paths": [
-                            {
-                                "path": "/",
-                                "backend": {
-                                    "serviceName": app_name,
-                                    "servicePort": port,
-                                },
-                            }
-                        ]
-                    },
-                }
-            ]
-        },
-    }
-    if ingress_spec_tls:
-        ingress["spec"]["tls"] = ingress_spec_tls
-
-    return [ingress]
-
-
-def _make_readiness_probe(port: int) -> Dict[str, Any]:
-    """Generate readiness probe.
-
-    Args:
-        port (int): service port.
-
-    Returns:
-        Dict[str, Any]: readiness probe.
-    """
-    return {
-        "httpGet": {
-            "path": "/api/health",
-            "port": port,
-        },
-        "initialDelaySeconds": 10,
-        "periodSeconds": 10,
-        "timeoutSeconds": 5,
-        "successThreshold": 1,
-        "failureThreshold": 3,
-    }
-
-
-def _make_liveness_probe(port: int) -> Dict[str, Any]:
-    """Generate liveness probe.
-
-    Args:
-        port (int): service port.
-
-    Returns:
-        Dict[str, Any]: liveness probe.
-    """
-    return {
-        "httpGet": {
-            "path": "/api/health",
-            "port": port,
-        },
-        "initialDelaySeconds": 60,
-        "timeoutSeconds": 30,
-        "failureThreshold": 10,
-    }
-
-
-def make_pod_spec(
-    image_info: Dict[str, str],
-    config: Dict[str, Any],
-    relation_state: Dict[str, Any],
-    app_name: str = "mongodb-exporter",
-    port: int = 9216,
-) -> Dict[str, Any]:
-    """Generate the pod spec information.
-
-    Args:
-        image_info (Dict[str, str]): Object provided by
-                                     OCIImageResource("image").fetch().
-        config (Dict[str, Any]): Configuration information.
-        relation_state (Dict[str, Any]): Relation state information.
-        app_name (str, optional): Application name. Defaults to "ro".
-        port (int, optional): Port for the container. Defaults to 9090.
-
-    Returns:
-        Dict[str, Any]: Pod spec dictionary for the charm.
-    """
-    if not image_info:
-        return None
-
-    _validate_data(config, relation_state)
-
-    ports = _make_pod_ports(port)
-    env_config = _make_pod_envconfig(config, relation_state)
-    readiness_probe = _make_readiness_probe(port)
-    liveness_probe = _make_liveness_probe(port)
-    ingress_resources = _make_pod_ingress_resources(config, app_name, port)
-
-    return {
-        "version": 3,
-        "containers": [
-            {
-                "name": app_name,
-                "imageDetails": image_info,
-                "imagePullPolicy": "Always",
-                "ports": ports,
-                "envConfig": env_config,
-                "kubernetes": {
-                    "readinessProbe": readiness_probe,
-                    "livenessProbe": liveness_probe,
-                },
-            }
-        ],
-        "kubernetesResources": {
-            "ingressResources": ingress_resources or [],
-        },
-    }
diff --git a/installers/charm/mongodb-exporter/templates/mongodb_exporter_dashboard.json b/installers/charm/mongodb-exporter/templates/mongodb_exporter_dashboard.json
deleted file mode 100644 (file)
index c6c64c2..0000000
+++ /dev/null
@@ -1,938 +0,0 @@
-{
-  "annotations": {
-    "list": [
-      {
-        "builtIn": 1,
-        "datasource": "-- Grafana --",
-        "enable": true,
-        "hide": true,
-        "iconColor": "rgba(0, 211, 255, 1)",
-        "name": "Annotations & Alerts",
-        "type": "dashboard"
-      }
-    ]
-  },
-  "description": "MongoDB Prometheus Exporter Dashboard.",
-  "editable": true,
-  "gnetId": 2583,
-  "graphTooltip": 1,
-  "id": 1,
-  "iteration": 1615141074039,
-  "links": [],
-  "panels": [
-    {
-      "collapsed": false,
-      "datasource": null,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 0
-      },
-      "id": 22,
-      "panels": [],
-      "repeat": "env",
-      "title": "Health",
-      "type": "row"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": false,
-      "colorValue": true,
-      "colors": [
-        "rgba(245, 54, 54, 0.9)",
-        "rgba(237, 129, 40, 0.89)",
-        "rgba(50, 172, 45, 0.97)"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "decimals": null,
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "s",
-      "gauge": {
-        "maxValue": 100,
-        "minValue": 0,
-        "show": false,
-        "thresholdLabels": false,
-        "thresholdMarkers": true
-      },
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 0,
-        "y": 1
-      },
-      "id": 10,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": false
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "mongodb_ss_uptime{}",
-          "format": "time_series",
-          "interval": "",
-          "intervalFactor": 2,
-          "legendFormat": "",
-          "refId": "A",
-          "step": 1800
-        }
-      ],
-      "thresholds": "0,360",
-      "title": "Uptime",
-      "type": "singlestat",
-      "valueFontSize": "80%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "N/A",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": false,
-      "colorValue": false,
-      "colors": [
-        "rgba(245, 54, 54, 0.9)",
-        "rgba(237, 129, 40, 0.89)",
-        "rgba(50, 172, 45, 0.97)"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {}
-        },
-        "overrides": []
-      },
-      "format": "none",
-      "gauge": {
-        "maxValue": 100,
-        "minValue": 0,
-        "show": false,
-        "thresholdLabels": false,
-        "thresholdMarkers": true
-      },
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 12,
-        "y": 1
-      },
-      "id": 1,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": true,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": true
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "mongodb_ss_connections{conn_type=\"current\"}",
-          "format": "time_series",
-          "interval": "",
-          "intervalFactor": 2,
-          "legendFormat": "",
-          "metric": "mongodb_connections",
-          "refId": "A",
-          "step": 1800
-        }
-      ],
-      "thresholds": "",
-      "title": "Open Connections",
-      "type": "singlestat",
-      "valueFontSize": "80%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "N/A",
-          "value": "null"
-        }
-      ],
-      "valueName": "avg"
-    },
-    {
-      "collapsed": false,
-      "datasource": null,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 5
-      },
-      "id": 20,
-      "panels": [],
-      "repeat": "env",
-      "title": "Operations",
-      "type": "row"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {},
-          "links": []
-        },
-        "overrides": []
-      },
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 6,
-        "w": 10,
-        "x": 0,
-        "y": 6
-      },
-      "hiddenSeries": false,
-      "id": 7,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "alertThreshold": true
-      },
-      "percentage": false,
-      "pluginVersion": "7.4.3",
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "rate(mongodb_ss_opcounters[$interval])",
-          "format": "time_series",
-          "interval": "",
-          "intervalFactor": 2,
-          "legendFormat": "{{legacy_op_type}}",
-          "refId": "A",
-          "step": 240
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Query Operations",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "$$hashKey": "object:670",
-          "format": "ops",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "$$hashKey": "object:671",
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {},
-          "links": []
-        },
-        "overrides": []
-      },
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 6,
-        "w": 8,
-        "x": 10,
-        "y": 6
-      },
-      "hiddenSeries": false,
-      "id": 9,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "alertThreshold": true
-      },
-      "percentage": false,
-      "pluginVersion": "7.4.3",
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [
-        {
-          "alias": "returned",
-          "yaxis": 1
-        }
-      ],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "rate(mongodb_ss_metrics_document[$interval])",
-          "format": "time_series",
-          "interval": "",
-          "intervalFactor": 2,
-          "legendFormat": "{{doc_op_type}}",
-          "refId": "A",
-          "step": 240
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Document Operations",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "$$hashKey": "object:699",
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "$$hashKey": "object:700",
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {},
-          "links": []
-        },
-        "overrides": []
-      },
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 6,
-        "w": 6,
-        "x": 18,
-        "y": 6
-      },
-      "hiddenSeries": false,
-      "id": 8,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "alertThreshold": true
-      },
-      "percentage": false,
-      "pluginVersion": "7.4.3",
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "rate(mongodb_ss_opcounters[$interval])",
-          "format": "time_series",
-          "interval": "",
-          "intervalFactor": 2,
-          "legendFormat": "{{legacy_op_type}}",
-          "refId": "A",
-          "step": 600
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Document Query Executor",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "$$hashKey": "object:728",
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "$$hashKey": "object:729",
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "collapsed": false,
-      "datasource": null,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 12
-      },
-      "id": 23,
-      "panels": [],
-      "repeat": null,
-      "title": "Resources",
-      "type": "row"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {},
-          "links": []
-        },
-        "overrides": []
-      },
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 6,
-        "w": 12,
-        "x": 0,
-        "y": 13
-      },
-      "hiddenSeries": false,
-      "id": 4,
-      "legend": {
-        "alignAsTable": false,
-        "avg": false,
-        "current": true,
-        "hideEmpty": false,
-        "hideZero": false,
-        "max": false,
-        "min": false,
-        "rightSide": false,
-        "show": true,
-        "total": false,
-        "values": true
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "alertThreshold": true
-      },
-      "percentage": false,
-      "pluginVersion": "7.4.3",
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "mongodb_ss_mem_resident",
-          "format": "time_series",
-          "interval": "",
-          "intervalFactor": 2,
-          "legendFormat": "Resident",
-          "refId": "A",
-          "step": 240
-        },
-        {
-          "expr": "mongodb_ss_mem_virtual",
-          "hide": false,
-          "interval": "",
-          "legendFormat": "Virtual",
-          "refId": "B"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Memory",
-      "tooltip": {
-        "shared": false,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": [
-          "total"
-        ]
-      },
-      "yaxes": [
-        {
-          "$$hashKey": "object:523",
-          "format": "decmbytes",
-          "label": "",
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "$$hashKey": "object:524",
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fieldConfig": {
-        "defaults": {
-          "custom": {},
-          "links": []
-        },
-        "overrides": []
-      },
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 6,
-        "w": 12,
-        "x": 12,
-        "y": 13
-      },
-      "hiddenSeries": false,
-      "id": 5,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "alertThreshold": true
-      },
-      "percentage": false,
-      "pluginVersion": "7.4.3",
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "rate(mongodb_ss_network_bytesOut[$interval])",
-          "format": "time_series",
-          "interval": "",
-          "intervalFactor": 2,
-          "legendFormat": "Out",
-          "metric": "mongodb_metrics_operation_total",
-          "refId": "A",
-          "step": 240
-        },
-        {
-          "expr": "rate(mongodb_ss_network_bytesIn[$interval])",
-          "hide": false,
-          "interval": "",
-          "intervalFactor": 2,
-          "legendFormat": "In",
-          "refId": "B"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Network I/O",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "$$hashKey": "object:579",
-          "format": "decbytes",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "$$hashKey": "object:580",
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    }
-  ],
-  "refresh": "5s",
-  "schemaVersion": 27,
-  "style": "dark",
-  "tags": [],
-  "templating": {
-    "list": [
-      {
-        "allValue": null,
-        "current": {
-          "selected": true,
-          "text": [
-            "All"
-          ],
-          "value": [
-            "$__all"
-          ]
-        },
-        "datasource": "prometheus - Juju generated source",
-        "definition": "",
-        "description": null,
-        "error": null,
-        "hide": 0,
-        "includeAll": true,
-        "label": "instance",
-        "multi": true,
-        "name": "instance",
-        "options": [],
-        "query": {
-          "query": "label_values(mongodb_connections, instance)",
-          "refId": "prometheus - Juju generated source-instance-Variable-Query"
-        },
-        "refresh": 1,
-        "regex": "",
-        "skipUrlSync": false,
-        "sort": 1,
-        "tagValuesQuery": "/.*-(.*?)-.*/",
-        "tags": [],
-        "tagsQuery": "label_values(mongodb_connections, instance)",
-        "type": "query",
-        "useTags": false
-      },
-      {
-        "auto": true,
-        "auto_count": 30,
-        "auto_min": "10s",
-        "current": {
-          "selected": false,
-          "text": "auto",
-          "value": "$__auto_interval_interval"
-        },
-        "description": null,
-        "error": null,
-        "hide": 0,
-        "label": null,
-        "name": "interval",
-        "options": [
-          {
-            "selected": true,
-            "text": "auto",
-            "value": "$__auto_interval_interval"
-          },
-          {
-            "selected": false,
-            "text": "1m",
-            "value": "1m"
-          },
-          {
-            "selected": false,
-            "text": "10m",
-            "value": "10m"
-          },
-          {
-            "selected": false,
-            "text": "30m",
-            "value": "30m"
-          },
-          {
-            "selected": false,
-            "text": "1h",
-            "value": "1h"
-          },
-          {
-            "selected": false,
-            "text": "6h",
-            "value": "6h"
-          },
-          {
-            "selected": false,
-            "text": "12h",
-            "value": "12h"
-          },
-          {
-            "selected": false,
-            "text": "1d",
-            "value": "1d"
-          },
-          {
-            "selected": false,
-            "text": "7d",
-            "value": "7d"
-          },
-          {
-            "selected": false,
-            "text": "14d",
-            "value": "14d"
-          },
-          {
-            "selected": false,
-            "text": "30d",
-            "value": "30d"
-          }
-        ],
-        "query": "1m,10m,30m,1h,6h,12h,1d,7d,14d,30d",
-        "refresh": 2,
-        "skipUrlSync": false,
-        "type": "interval"
-      }
-    ]
-  },
-  "time": {
-    "from": "now/d",
-    "to": "now"
-  },
-  "timepicker": {
-    "refresh_intervals": [
-      "5s",
-      "10s",
-      "30s",
-      "1m",
-      "5m",
-      "15m",
-      "30m",
-      "1h",
-      "2h",
-      "1d"
-    ],
-    "time_options": [
-      "5m",
-      "15m",
-      "1h",
-      "6h",
-      "12h",
-      "24h",
-      "2d",
-      "7d",
-      "30d"
-    ]
-  },
-  "timezone": "browser",
-  "title": "MongoDB",
-  "uid": "HEK4NbtZk",
-  "version": 17
-}
\ No newline at end of file
diff --git a/installers/charm/mongodb-exporter/tests/__init__.py b/installers/charm/mongodb-exporter/tests/__init__.py
deleted file mode 100644 (file)
index 90dc417..0000000
+++ /dev/null
@@ -1,39 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-"""Init mocking for unit tests."""
-
-import sys
-
-import mock
-
-
-class OCIImageResourceErrorMock(Exception):
-    pass
-
-
-sys.path.append("src")
-
-oci_image = mock.MagicMock()
-oci_image.OCIImageResourceError = OCIImageResourceErrorMock
-sys.modules["oci_image"] = oci_image
-sys.modules["oci_image"].OCIImageResource().fetch.return_value = {}
diff --git a/installers/charm/mongodb-exporter/tests/test_charm.py b/installers/charm/mongodb-exporter/tests/test_charm.py
deleted file mode 100644 (file)
index 1675f5f..0000000
+++ /dev/null
@@ -1,583 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-import sys
-from typing import NoReturn
-import unittest
-
-from charm import MongodbExporterCharm
-from ops.model import ActiveStatus, BlockedStatus
-from ops.testing import Harness
-
-
-class TestCharm(unittest.TestCase):
-    """Mongodb Exporter Charm unit tests."""
-
-    def setUp(self) -> NoReturn:
-        """Test setup"""
-        self.image_info = sys.modules["oci_image"].OCIImageResource().fetch()
-        self.harness = Harness(MongodbExporterCharm)
-        self.harness.set_leader(is_leader=True)
-        self.harness.begin()
-        self.config = {
-            "ingress_whitelist_source_range": "",
-            "tls_secret_name": "",
-            "site_url": "https://mongodb-exporter.192.168.100.100.nip.io",
-            "cluster_issuer": "vault-issuer",
-        }
-        self.harness.update_config(self.config)
-
-    def test_config_changed_no_relations(
-        self,
-    ) -> NoReturn:
-        """Test ingress resources without HTTP."""
-
-        self.harness.charm.on.config_changed.emit()
-
-        # Assertions
-        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
-        print(self.harness.charm.unit.status.message)
-        self.assertTrue(
-            all(
-                relation in self.harness.charm.unit.status.message
-                for relation in ["mongodb"]
-            )
-        )
-
-    def test_config_changed_non_leader(
-        self,
-    ) -> NoReturn:
-        """Test ingress resources without HTTP."""
-        self.harness.set_leader(is_leader=False)
-        self.harness.charm.on.config_changed.emit()
-
-        # Assertions
-        self.assertIsInstance(self.harness.charm.unit.status, ActiveStatus)
-
-    def test_with_relations(
-        self,
-    ) -> NoReturn:
-        "Test with relations"
-        self.initialize_mongo_relation()
-
-        # Verifying status
-        self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
-
-    def test_with_config(
-        self,
-    ) -> NoReturn:
-        "Test with config"
-        self.initialize_mongo_relation()
-
-        # Verifying status
-        self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
-
-    def test_mongodb_exception_relation_and_config(
-        self,
-    ) -> NoReturn:
-        self.initialize_mongo_config()
-        self.initialize_mongo_relation()
-
-        # Verifying status
-        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
-
-    def initialize_mongo_relation(self):
-        mongodb_relation_id = self.harness.add_relation("mongodb", "mongodb")
-        self.harness.add_relation_unit(mongodb_relation_id, "mongodb/0")
-        self.harness.update_relation_data(
-            mongodb_relation_id,
-            "mongodb/0",
-            {"connection_string": "mongodb://mongo:27017"},
-        )
-
-    def initialize_mongo_config(self):
-        self.harness.update_config({"mongodb_uri": "mongodb://mongo:27017"})
-
-
-if __name__ == "__main__":
-    unittest.main()
-
-
-# class TestCharm(unittest.TestCase):
-#    """Mongodb Exporter Charm unit tests."""
-#
-#    def setUp(self) -> NoReturn:
-#        """Test setup"""
-#        self.harness = Harness(MongodbExporterCharm)
-#        self.harness.set_leader(is_leader=True)
-#        self.harness.begin()
-#
-#    def test_on_start_without_relations(self) -> NoReturn:
-#        """Test installation without any relation."""
-#        self.harness.charm.on.start.emit()
-#
-#        # Verifying status
-#        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
-#
-#        # Verifying status message
-#        self.assertGreater(len(self.harness.charm.unit.status.message), 0)
-#        self.assertTrue(
-#            self.harness.charm.unit.status.message.startswith("Waiting for ")
-#        )
-#        self.assertIn("mongodb", self.harness.charm.unit.status.message)
-#        self.assertTrue(self.harness.charm.unit.status.message.endswith(" relation"))
-#
-#    def test_on_start_with_relations_without_http(self) -> NoReturn:
-#        """Test deployment."""
-#        expected_result = {
-#            "version": 3,
-#            "containers": [
-#                {
-#                    "name": "mongodb-exporter",
-#                    "imageDetails": self.harness.charm.image.fetch(),
-#                    "imagePullPolicy": "Always",
-#                    "ports": [
-#                        {
-#                            "name": "mongo-exporter",
-#                            "containerPort": 9216,
-#                            "protocol": "TCP",
-#                        }
-#                    ],
-#                    "envConfig": {
-#                        "MONGODB_URI": "mongodb://mongo",
-#                    },
-#                    "kubernetes": {
-#                        "readinessProbe": {
-#                            "httpGet": {
-#                                "path": "/api/health",
-#                                "port": 9216,
-#                            },
-#                            "initialDelaySeconds": 10,
-#                            "periodSeconds": 10,
-#                            "timeoutSeconds": 5,
-#                            "successThreshold": 1,
-#                            "failureThreshold": 3,
-#                        },
-#                        "livenessProbe": {
-#                            "httpGet": {
-#                                "path": "/api/health",
-#                                "port": 9216,
-#                            },
-#                            "initialDelaySeconds": 60,
-#                            "timeoutSeconds": 30,
-#                            "failureThreshold": 10,
-#                        },
-#                    },
-#                },
-#            ],
-#            "kubernetesResources": {"ingressResources": []},
-#        }
-#
-#        self.harness.charm.on.start.emit()
-#
-#        # Initializing the mongodb relation
-#        relation_id = self.harness.add_relation("mongodb", "mongodb")
-#        self.harness.add_relation_unit(relation_id, "mongodb/0")
-#        self.harness.update_relation_data(
-#            relation_id,
-#            "mongodb/0",
-#            {
-#                "connection_string": "mongodb://mongo",
-#            },
-#        )
-#
-#        # Verifying status
-#        self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
-#
-#        pod_spec, _ = self.harness.get_pod_spec()
-#
-#        self.assertDictEqual(expected_result, pod_spec)
-#
-#    def test_ingress_resources_with_http(self) -> NoReturn:
-#        """Test ingress resources with HTTP."""
-#        expected_result = {
-#            "version": 3,
-#            "containers": [
-#                {
-#                    "name": "mongodb-exporter",
-#                    "imageDetails": self.harness.charm.image.fetch(),
-#                    "imagePullPolicy": "Always",
-#                    "ports": [
-#                        {
-#                            "name": "mongo-exporter",
-#                            "containerPort": 9216,
-#                            "protocol": "TCP",
-#                        }
-#                    ],
-#                    "envConfig": {
-#                        "MONGODB_URI": "mongodb://mongo",
-#                    },
-#                    "kubernetes": {
-#                        "readinessProbe": {
-#                            "httpGet": {
-#                                "path": "/api/health",
-#                                "port": 9216,
-#                            },
-#                            "initialDelaySeconds": 10,
-#                            "periodSeconds": 10,
-#                            "timeoutSeconds": 5,
-#                            "successThreshold": 1,
-#                            "failureThreshold": 3,
-#                        },
-#                        "livenessProbe": {
-#                            "httpGet": {
-#                                "path": "/api/health",
-#                                "port": 9216,
-#                            },
-#                            "initialDelaySeconds": 60,
-#                            "timeoutSeconds": 30,
-#                            "failureThreshold": 10,
-#                        },
-#                    },
-#                },
-#            ],
-#            "kubernetesResources": {
-#                "ingressResources": [
-#                    {
-#                        "name": "mongodb-exporter-ingress",
-#                        "annotations": {
-#                            "nginx.ingress.kubernetes.io/ssl-redirect": "false",
-#                        },
-#                        "spec": {
-#                            "rules": [
-#                                {
-#                                    "host": "mongodb-exporter",
-#                                    "http": {
-#                                        "paths": [
-#                                            {
-#                                                "path": "/",
-#                                                "backend": {
-#                                                    "serviceName": "mongodb-exporter",
-#                                                    "servicePort": 9216,
-#                                                },
-#                                            }
-#                                        ]
-#                                    },
-#                                }
-#                            ]
-#                        },
-#                    }
-#                ],
-#            },
-#        }
-#
-#        self.harness.charm.on.start.emit()
-#
-#        # Initializing the mongodb relation
-#        relation_id = self.harness.add_relation("mongodb", "mongodb")
-#        self.harness.add_relation_unit(relation_id, "mongodb/0")
-#        self.harness.update_relation_data(
-#            relation_id,
-#            "mongodb/0",
-#            {
-#                "connection_string": "mongodb://mongo",
-#            },
-#        )
-#
-#        self.harness.update_config({"site_url": "http://mongodb-exporter"})
-#
-#        pod_spec, _ = self.harness.get_pod_spec()
-#
-#        self.assertDictEqual(expected_result, pod_spec)
-#
-#    def test_ingress_resources_with_https(self) -> NoReturn:
-#        """Test ingress resources with HTTPS."""
-#        expected_result = {
-#            "version": 3,
-#            "containers": [
-#                {
-#                    "name": "mongodb-exporter",
-#                    "imageDetails": self.harness.charm.image.fetch(),
-#                    "imagePullPolicy": "Always",
-#                    "ports": [
-#                        {
-#                            "name": "mongo-exporter",
-#                            "containerPort": 9216,
-#                            "protocol": "TCP",
-#                        }
-#                    ],
-#                    "envConfig": {
-#                        "MONGODB_URI": "mongodb://mongo",
-#                    },
-#                    "kubernetes": {
-#                        "readinessProbe": {
-#                            "httpGet": {
-#                                "path": "/api/health",
-#                                "port": 9216,
-#                            },
-#                            "initialDelaySeconds": 10,
-#                            "periodSeconds": 10,
-#                            "timeoutSeconds": 5,
-#                            "successThreshold": 1,
-#                            "failureThreshold": 3,
-#                        },
-#                        "livenessProbe": {
-#                            "httpGet": {
-#                                "path": "/api/health",
-#                                "port": 9216,
-#                            },
-#                            "initialDelaySeconds": 60,
-#                            "timeoutSeconds": 30,
-#                            "failureThreshold": 10,
-#                        },
-#                    },
-#                },
-#            ],
-#            "kubernetesResources": {
-#                "ingressResources": [
-#                    {
-#                        "name": "mongodb-exporter-ingress",
-#                        "annotations": {},
-#                        "spec": {
-#                            "rules": [
-#                                {
-#                                    "host": "mongodb-exporter",
-#                                    "http": {
-#                                        "paths": [
-#                                            {
-#                                                "path": "/",
-#                                                "backend": {
-#                                                    "serviceName": "mongodb-exporter",
-#                                                    "servicePort": 9216,
-#                                                },
-#                                            }
-#                                        ]
-#                                    },
-#                                }
-#                            ],
-#                            "tls": [
-#                                {
-#                                    "hosts": ["mongodb-exporter"],
-#                                    "secretName": "mongodb-exporter",
-#                                }
-#                            ],
-#                        },
-#                    }
-#                ],
-#            },
-#        }
-#
-#        self.harness.charm.on.start.emit()
-#
-#        # Initializing the mongodb relation
-#        relation_id = self.harness.add_relation("mongodb", "mongodb")
-#        self.harness.add_relation_unit(relation_id, "mongodb/0")
-#        self.harness.update_relation_data(
-#            relation_id,
-#            "mongodb/0",
-#            {
-#                "connection_string": "mongodb://mongo",
-#            },
-#        )
-#
-#        self.harness.update_config(
-#            {
-#                "site_url": "https://mongodb-exporter",
-#                "tls_secret_name": "mongodb-exporter",
-#            }
-#        )
-#
-#        pod_spec, _ = self.harness.get_pod_spec()
-#
-#        self.assertDictEqual(expected_result, pod_spec)
-#
-#    def test_ingress_resources_with_https_and_ingress_whitelist(self) -> NoReturn:
-#        """Test ingress resources with HTTPS and ingress whitelist."""
-#        expected_result = {
-#            "version": 3,
-#            "containers": [
-#                {
-#                    "name": "mongodb-exporter",
-#                    "imageDetails": self.harness.charm.image.fetch(),
-#                    "imagePullPolicy": "Always",
-#                    "ports": [
-#                        {
-#                            "name": "mongo-exporter",
-#                            "containerPort": 9216,
-#                            "protocol": "TCP",
-#                        }
-#                    ],
-#                    "envConfig": {
-#                        "MONGODB_URI": "mongodb://mongo",
-#                    },
-#                    "kubernetes": {
-#                        "readinessProbe": {
-#                            "httpGet": {
-#                                "path": "/api/health",
-#                                "port": 9216,
-#                            },
-#                            "initialDelaySeconds": 10,
-#                            "periodSeconds": 10,
-#                            "timeoutSeconds": 5,
-#                            "successThreshold": 1,
-#                            "failureThreshold": 3,
-#                        },
-#                        "livenessProbe": {
-#                            "httpGet": {
-#                                "path": "/api/health",
-#                                "port": 9216,
-#                            },
-#                            "initialDelaySeconds": 60,
-#                            "timeoutSeconds": 30,
-#                            "failureThreshold": 10,
-#                        },
-#                    },
-#                },
-#            ],
-#            "kubernetesResources": {
-#                "ingressResources": [
-#                    {
-#                        "name": "mongodb-exporter-ingress",
-#                        "annotations": {
-#                            "nginx.ingress.kubernetes.io/whitelist-source-range": "0.0.0.0/0",
-#                        },
-#                        "spec": {
-#                            "rules": [
-#                                {
-#                                    "host": "mongodb-exporter",
-#                                    "http": {
-#                                        "paths": [
-#                                            {
-#                                                "path": "/",
-#                                                "backend": {
-#                                                    "serviceName": "mongodb-exporter",
-#                                                    "servicePort": 9216,
-#                                                },
-#                                            }
-#                                        ]
-#                                    },
-#                                }
-#                            ],
-#                            "tls": [
-#                                {
-#                                    "hosts": ["mongodb-exporter"],
-#                                    "secretName": "mongodb-exporter",
-#                                }
-#                            ],
-#                        },
-#                    }
-#                ],
-#            },
-#        }
-#
-#        self.harness.charm.on.start.emit()
-#
-#        # Initializing the mongodb relation
-#        relation_id = self.harness.add_relation("mongodb", "mongodb")
-#        self.harness.add_relation_unit(relation_id, "mongodb/0")
-#        self.harness.update_relation_data(
-#            relation_id,
-#            "mongodb/0",
-#            {
-#                "connection_string": "mongodb://mongo",
-#            },
-#        )
-#
-#        self.harness.update_config(
-#            {
-#                "site_url": "https://mongodb-exporter",
-#                "tls_secret_name": "mongodb-exporter",
-#                "ingress_whitelist_source_range": "0.0.0.0/0",
-#            }
-#        )
-#
-#        pod_spec, _ = self.harness.get_pod_spec()
-#
-#        self.assertDictEqual(expected_result, pod_spec)
-#
-#    def test_on_mongodb_unit_relation_changed(self) -> NoReturn:
-#        """Test to see if mongodb relation is updated."""
-#        self.harness.charm.on.start.emit()
-#
-#        # Initializing the mongodb relation
-#        relation_id = self.harness.add_relation("mongodb", "mongodb")
-#        self.harness.add_relation_unit(relation_id, "mongodb/0")
-#        self.harness.update_relation_data(
-#            relation_id,
-#            "mongodb/0",
-#            {
-#                "connection_string": "mongodb://mongo",
-#            },
-#        )
-#
-#        # Verifying status
-#        self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
-#
-#    def test_publish_scrape_info(self) -> NoReturn:
-#        """Test to see if scrape relation is updated."""
-#        expected_result = {
-#            "hostname": "mongodb-exporter",
-#            "port": "9216",
-#            "metrics_path": "/metrics",
-#            "scrape_interval": "30s",
-#            "scrape_timeout": "15s",
-#        }
-#
-#        self.harness.charm.on.start.emit()
-#
-#        relation_id = self.harness.add_relation("prometheus-scrape", "prometheus")
-#        self.harness.add_relation_unit(relation_id, "prometheus/0")
-#        relation_data = self.harness.get_relation_data(
-#            relation_id, "mongodb-exporter/0"
-#        )
-#
-#        self.assertDictEqual(expected_result, relation_data)
-#
-#    def test_publish_scrape_info_with_site_url(self) -> NoReturn:
-#        """Test to see if target relation is updated."""
-#        expected_result = {
-#            "hostname": "mongodb-exporter-osm",
-#            "port": "80",
-#            "metrics_path": "/metrics",
-#            "scrape_interval": "30s",
-#            "scrape_timeout": "15s",
-#        }
-#
-#        self.harness.charm.on.start.emit()
-#
-#        self.harness.update_config({"site_url": "http://mongodb-exporter-osm"})
-#
-#        relation_id = self.harness.add_relation("prometheus-scrape", "prometheus")
-#        self.harness.add_relation_unit(relation_id, "prometheus/0")
-#        relation_data = self.harness.get_relation_data(
-#            relation_id, "mongodb-exporter/0"
-#        )
-#
-#        self.assertDictEqual(expected_result, relation_data)
-#
-#    def test_publish_dashboard_info(self) -> NoReturn:
-#        """Test to see if dashboard relation is updated."""
-#        self.harness.charm.on.start.emit()
-#
-#        relation_id = self.harness.add_relation("grafana-dashboard", "grafana")
-#        self.harness.add_relation_unit(relation_id, "grafana/0")
-#        relation_data = self.harness.get_relation_data(
-#            relation_id, "mongodb-exporter/0"
-#        )
-#
-#        self.assertEqual("osm-mongodb", relation_data["name"])
-#        self.assertTrue("dashboard" in relation_data)
-#        self.assertTrue(len(relation_data["dashboard"]) > 0)
-#
-#
-# if __name__ == "__main__":
-#    unittest.main()
diff --git a/installers/charm/mongodb-exporter/tests/test_pod_spec.py b/installers/charm/mongodb-exporter/tests/test_pod_spec.py
deleted file mode 100644 (file)
index 94ab6fb..0000000
+++ /dev/null
@@ -1,489 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-from typing import NoReturn
-import unittest
-
-import pod_spec
-
-
-class TestPodSpec(unittest.TestCase):
-    """Pod spec unit tests."""
-
-    def test_make_pod_ports(self) -> NoReturn:
-        """Testing make pod ports."""
-        port = 9216
-
-        expected_result = [
-            {
-                "name": "mongo-exporter",
-                "containerPort": port,
-                "protocol": "TCP",
-            }
-        ]
-
-        pod_ports = pod_spec._make_pod_ports(port)
-
-        self.assertListEqual(expected_result, pod_ports)
-
-    def test_make_pod_envconfig(self) -> NoReturn:
-        """Teting make pod envconfig."""
-        config = {}
-        relation_state = {
-            "mongodb_connection_string": "mongodb://mongo",
-        }
-
-        expected_result = {"MONGODB_URI": "mongodb://mongo"}
-
-        pod_envconfig = pod_spec._make_pod_envconfig(config, relation_state)
-
-        self.assertDictEqual(expected_result, pod_envconfig)
-
-    def test_make_pod_ingress_resources_without_site_url(self) -> NoReturn:
-        """Testing make pod ingress resources without site_url."""
-        config = {
-            "site_url": "",
-            "cluster_issuer": "",
-        }
-        app_name = "mongodb-exporter"
-        port = 9216
-
-        pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-            config, app_name, port
-        )
-
-        self.assertIsNone(pod_ingress_resources)
-
-    def test_make_pod_ingress_resources(self) -> NoReturn:
-        """Testing make pod ingress resources."""
-        config = {
-            "site_url": "http://mongodb-exporter",
-            "cluster_issuer": "",
-            "ingress_whitelist_source_range": "",
-        }
-        app_name = "mongodb-exporter"
-        port = 9216
-
-        expected_result = [
-            {
-                "name": f"{app_name}-ingress",
-                "annotations": {
-                    "nginx.ingress.kubernetes.io/ssl-redirect": "false",
-                },
-                "spec": {
-                    "rules": [
-                        {
-                            "host": app_name,
-                            "http": {
-                                "paths": [
-                                    {
-                                        "path": "/",
-                                        "backend": {
-                                            "serviceName": app_name,
-                                            "servicePort": port,
-                                        },
-                                    }
-                                ]
-                            },
-                        }
-                    ]
-                },
-            }
-        ]
-
-        pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-            config, app_name, port
-        )
-
-        self.assertListEqual(expected_result, pod_ingress_resources)
-
-    def test_make_pod_ingress_resources_with_whitelist_source_range(self) -> NoReturn:
-        """Testing make pod ingress resources with whitelist_source_range."""
-        config = {
-            "site_url": "http://mongodb-exporter",
-            "cluster_issuer": "",
-            "ingress_whitelist_source_range": "0.0.0.0/0",
-        }
-        app_name = "mongodb-exporter"
-        port = 9216
-
-        expected_result = [
-            {
-                "name": f"{app_name}-ingress",
-                "annotations": {
-                    "nginx.ingress.kubernetes.io/ssl-redirect": "false",
-                    "nginx.ingress.kubernetes.io/whitelist-source-range": config[
-                        "ingress_whitelist_source_range"
-                    ],
-                },
-                "spec": {
-                    "rules": [
-                        {
-                            "host": app_name,
-                            "http": {
-                                "paths": [
-                                    {
-                                        "path": "/",
-                                        "backend": {
-                                            "serviceName": app_name,
-                                            "servicePort": port,
-                                        },
-                                    }
-                                ]
-                            },
-                        }
-                    ]
-                },
-            }
-        ]
-
-        pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-            config, app_name, port
-        )
-
-        self.assertListEqual(expected_result, pod_ingress_resources)
-
-    def test_make_pod_ingress_resources_with_https(self) -> NoReturn:
-        """Testing make pod ingress resources with HTTPs."""
-        config = {
-            "site_url": "https://mongodb-exporter",
-            "cluster_issuer": "",
-            "ingress_whitelist_source_range": "",
-            "tls_secret_name": "",
-        }
-        app_name = "mongodb-exporter"
-        port = 9216
-
-        expected_result = [
-            {
-                "name": f"{app_name}-ingress",
-                "annotations": {},
-                "spec": {
-                    "rules": [
-                        {
-                            "host": app_name,
-                            "http": {
-                                "paths": [
-                                    {
-                                        "path": "/",
-                                        "backend": {
-                                            "serviceName": app_name,
-                                            "servicePort": port,
-                                        },
-                                    }
-                                ]
-                            },
-                        }
-                    ],
-                    "tls": [{"hosts": [app_name]}],
-                },
-            }
-        ]
-
-        pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-            config, app_name, port
-        )
-
-        self.assertListEqual(expected_result, pod_ingress_resources)
-
-    def test_make_pod_ingress_resources_with_https_tls_secret_name(self) -> NoReturn:
-        """Testing make pod ingress resources with HTTPs and TLS secret name."""
-        config = {
-            "site_url": "https://mongodb-exporter",
-            "cluster_issuer": "",
-            "ingress_whitelist_source_range": "",
-            "tls_secret_name": "secret_name",
-        }
-        app_name = "mongodb-exporter"
-        port = 9216
-
-        expected_result = [
-            {
-                "name": f"{app_name}-ingress",
-                "annotations": {},
-                "spec": {
-                    "rules": [
-                        {
-                            "host": app_name,
-                            "http": {
-                                "paths": [
-                                    {
-                                        "path": "/",
-                                        "backend": {
-                                            "serviceName": app_name,
-                                            "servicePort": port,
-                                        },
-                                    }
-                                ]
-                            },
-                        }
-                    ],
-                    "tls": [
-                        {"hosts": [app_name], "secretName": config["tls_secret_name"]}
-                    ],
-                },
-            }
-        ]
-
-        pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-            config, app_name, port
-        )
-
-        self.assertListEqual(expected_result, pod_ingress_resources)
-
-    def test_make_readiness_probe(self) -> NoReturn:
-        """Testing make readiness probe."""
-        port = 9216
-
-        expected_result = {
-            "httpGet": {
-                "path": "/api/health",
-                "port": port,
-            },
-            "initialDelaySeconds": 10,
-            "periodSeconds": 10,
-            "timeoutSeconds": 5,
-            "successThreshold": 1,
-            "failureThreshold": 3,
-        }
-
-        readiness_probe = pod_spec._make_readiness_probe(port)
-
-        self.assertDictEqual(expected_result, readiness_probe)
-
-    def test_make_liveness_probe(self) -> NoReturn:
-        """Testing make liveness probe."""
-        port = 9216
-
-        expected_result = {
-            "httpGet": {
-                "path": "/api/health",
-                "port": port,
-            },
-            "initialDelaySeconds": 60,
-            "timeoutSeconds": 30,
-            "failureThreshold": 10,
-        }
-
-        liveness_probe = pod_spec._make_liveness_probe(port)
-
-        self.assertDictEqual(expected_result, liveness_probe)
-
-    def test_make_pod_spec(self) -> NoReturn:
-        """Testing make pod spec."""
-        image_info = {"upstream-source": "bitnami/mongodb-exporter:latest"}
-        config = {
-            "site_url": "",
-            "cluster_issuer": "",
-        }
-        relation_state = {
-            "mongodb_connection_string": "mongodb://mongo",
-        }
-        app_name = "mongodb-exporter"
-        port = 9216
-
-        expected_result = {
-            "version": 3,
-            "containers": [
-                {
-                    "name": app_name,
-                    "imageDetails": image_info,
-                    "imagePullPolicy": "Always",
-                    "ports": [
-                        {
-                            "name": "mongo-exporter",
-                            "containerPort": port,
-                            "protocol": "TCP",
-                        }
-                    ],
-                    "envConfig": {
-                        "MONGODB_URI": "mongodb://mongo",
-                    },
-                    "kubernetes": {
-                        "readinessProbe": {
-                            "httpGet": {
-                                "path": "/api/health",
-                                "port": port,
-                            },
-                            "initialDelaySeconds": 10,
-                            "periodSeconds": 10,
-                            "timeoutSeconds": 5,
-                            "successThreshold": 1,
-                            "failureThreshold": 3,
-                        },
-                        "livenessProbe": {
-                            "httpGet": {
-                                "path": "/api/health",
-                                "port": port,
-                            },
-                            "initialDelaySeconds": 60,
-                            "timeoutSeconds": 30,
-                            "failureThreshold": 10,
-                        },
-                    },
-                }
-            ],
-            "kubernetesResources": {"ingressResources": []},
-        }
-
-        spec = pod_spec.make_pod_spec(
-            image_info, config, relation_state, app_name, port
-        )
-
-        self.assertDictEqual(expected_result, spec)
-
-    def test_make_pod_spec_with_ingress(self) -> NoReturn:
-        """Testing make pod spec."""
-        image_info = {"upstream-source": "bitnami/mongodb-exporter:latest"}
-        config = {
-            "site_url": "https://mongodb-exporter",
-            "cluster_issuer": "",
-            "tls_secret_name": "mongodb-exporter",
-            "ingress_whitelist_source_range": "0.0.0.0/0",
-        }
-        relation_state = {
-            "mongodb_connection_string": "mongodb://mongo",
-        }
-        app_name = "mongodb-exporter"
-        port = 9216
-
-        expected_result = {
-            "version": 3,
-            "containers": [
-                {
-                    "name": app_name,
-                    "imageDetails": image_info,
-                    "imagePullPolicy": "Always",
-                    "ports": [
-                        {
-                            "name": "mongo-exporter",
-                            "containerPort": port,
-                            "protocol": "TCP",
-                        }
-                    ],
-                    "envConfig": {
-                        "MONGODB_URI": "mongodb://mongo",
-                    },
-                    "kubernetes": {
-                        "readinessProbe": {
-                            "httpGet": {
-                                "path": "/api/health",
-                                "port": port,
-                            },
-                            "initialDelaySeconds": 10,
-                            "periodSeconds": 10,
-                            "timeoutSeconds": 5,
-                            "successThreshold": 1,
-                            "failureThreshold": 3,
-                        },
-                        "livenessProbe": {
-                            "httpGet": {
-                                "path": "/api/health",
-                                "port": port,
-                            },
-                            "initialDelaySeconds": 60,
-                            "timeoutSeconds": 30,
-                            "failureThreshold": 10,
-                        },
-                    },
-                }
-            ],
-            "kubernetesResources": {
-                "ingressResources": [
-                    {
-                        "name": "{}-ingress".format(app_name),
-                        "annotations": {
-                            "nginx.ingress.kubernetes.io/whitelist-source-range": config.get(
-                                "ingress_whitelist_source_range"
-                            ),
-                        },
-                        "spec": {
-                            "rules": [
-                                {
-                                    "host": app_name,
-                                    "http": {
-                                        "paths": [
-                                            {
-                                                "path": "/",
-                                                "backend": {
-                                                    "serviceName": app_name,
-                                                    "servicePort": port,
-                                                },
-                                            }
-                                        ]
-                                    },
-                                }
-                            ],
-                            "tls": [
-                                {
-                                    "hosts": [app_name],
-                                    "secretName": config.get("tls_secret_name"),
-                                }
-                            ],
-                        },
-                    }
-                ],
-            },
-        }
-
-        spec = pod_spec.make_pod_spec(
-            image_info, config, relation_state, app_name, port
-        )
-
-        self.assertDictEqual(expected_result, spec)
-
-    def test_make_pod_spec_without_image_info(self) -> NoReturn:
-        """Testing make pod spec without image_info."""
-        image_info = None
-        config = {
-            "site_url": "",
-            "cluster_issuer": "",
-        }
-        relation_state = {
-            "mongodb_connection_string": "mongodb://mongo",
-        }
-        app_name = "mongodb-exporter"
-        port = 9216
-
-        spec = pod_spec.make_pod_spec(
-            image_info, config, relation_state, app_name, port
-        )
-
-        self.assertIsNone(spec)
-
-    def test_make_pod_spec_without_relation_state(self) -> NoReturn:
-        """Testing make pod spec without relation_state."""
-        image_info = {"upstream-source": "bitnami/mongodb-exporter:latest"}
-        config = {
-            "site_url": "",
-            "cluster_issuer": "",
-        }
-        relation_state = {}
-        app_name = "mongodb-exporter"
-        port = 9216
-
-        with self.assertRaises(ValueError):
-            pod_spec.make_pod_spec(image_info, config, relation_state, app_name, port)
-
-
-if __name__ == "__main__":
-    unittest.main()
diff --git a/installers/charm/mongodb-exporter/tox.ini b/installers/charm/mongodb-exporter/tox.ini
deleted file mode 100644 (file)
index 4c7970d..0000000
+++ /dev/null
@@ -1,126 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-#######################################################################################
-
-[tox]
-envlist = black, cover, flake8, pylint, yamllint, safety
-skipsdist = true
-
-[tox:jenkins]
-toxworkdir = /tmp/.tox
-
-[testenv]
-basepython = python3.8
-setenv = VIRTUAL_ENV={envdir}
-         PYTHONDONTWRITEBYTECODE = 1
-deps =  -r{toxinidir}/requirements.txt
-
-
-#######################################################################################
-[testenv:black]
-deps = black
-commands =
-        black --check --diff src/ tests/
-
-
-#######################################################################################
-[testenv:cover]
-deps =  {[testenv]deps}
-        -r{toxinidir}/requirements-test.txt
-        coverage
-        nose2
-commands =
-        sh -c 'rm -f nosetests.xml'
-        coverage erase
-        nose2 -C --coverage src
-        coverage report --omit='*tests*'
-        coverage html -d ./cover --omit='*tests*'
-        coverage xml -o coverage.xml --omit=*tests*
-whitelist_externals = sh
-
-
-#######################################################################################
-[testenv:flake8]
-deps =  flake8
-        flake8-import-order
-commands =
-        flake8 src/ tests/
-
-
-#######################################################################################
-[testenv:pylint]
-deps =  {[testenv]deps}
-        -r{toxinidir}/requirements-test.txt
-        pylint==2.10.2
-commands =
-    pylint -E src/ tests/
-
-
-#######################################################################################
-[testenv:safety]
-setenv =
-        LC_ALL=C.UTF-8
-        LANG=C.UTF-8
-deps =  {[testenv]deps}
-        safety
-commands =
-        - safety check --full-report
-
-
-#######################################################################################
-[testenv:yamllint]
-deps =  {[testenv]deps}
-        -r{toxinidir}/requirements-test.txt
-        yamllint
-commands = yamllint .
-
-#######################################################################################
-[testenv:build]
-passenv=HTTP_PROXY HTTPS_PROXY NO_PROXY
-whitelist_externals =
-  charmcraft
-  sh
-commands =
-  charmcraft pack
-  sh -c 'ubuntu_version=20.04; \
-        architectures="amd64-aarch64-arm64"; \
-        charm_name=`cat metadata.yaml | grep -E "^name: " | cut -f 2 -d " "`; \
-        mv $charm_name"_ubuntu-"$ubuntu_version-$architectures.charm $charm_name.charm'
-
-#######################################################################################
-[flake8]
-ignore =
-        W291,
-        W293,
-        W503,
-        E123,
-        E125,
-        E226,
-        E241,
-exclude =
-        .git,
-        __pycache__,
-        .tox,
-max-line-length = 120
-show-source = True
-builtins = _
-max-complexity = 10
-import-order-style = google
diff --git a/installers/charm/mysqld-exporter/.gitignore b/installers/charm/mysqld-exporter/.gitignore
deleted file mode 100644 (file)
index 2885df2..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-venv
-.vscode
-build
-*.charm
-.coverage
-coverage.xml
-.stestr
-cover
-release
\ No newline at end of file
diff --git a/installers/charm/mysqld-exporter/.jujuignore b/installers/charm/mysqld-exporter/.jujuignore
deleted file mode 100644 (file)
index 3ae3e7d..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-venv
-.vscode
-build
-*.charm
-.coverage
-coverage.xml
-.gitignore
-.stestr
-cover
-release
-tests/
-requirements*
-tox.ini
diff --git a/installers/charm/mysqld-exporter/.yamllint.yaml b/installers/charm/mysqld-exporter/.yamllint.yaml
deleted file mode 100644 (file)
index d71fb69..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
----
-extends: default
-
-yaml-files:
-  - "*.yaml"
-  - "*.yml"
-  - ".yamllint"
-ignore: |
-  .tox
-  cover/
-  build/
-  venv
-  release/
diff --git a/installers/charm/mysqld-exporter/README.md b/installers/charm/mysqld-exporter/README.md
deleted file mode 100644 (file)
index 481d53c..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<!-- 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.
-
-For those usages not covered by the Apache License, Version 2.0 please
-contact: legal@canonical.com
-
-To get in touch with the maintainers, please contact:
-osm-charmers@lists.launchpad.net -->
-
-# Prometheus Mysql Exporter operator Charm for Kubernetes
-
-## Requirements
diff --git a/installers/charm/mysqld-exporter/charmcraft.yaml b/installers/charm/mysqld-exporter/charmcraft.yaml
deleted file mode 100644 (file)
index 0a285a9..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-type: charm
-bases:
-  - build-on:
-      - name: ubuntu
-        channel: "20.04"
-        architectures: ["amd64"]
-    run-on:
-      - name: ubuntu
-        channel: "20.04"
-        architectures:
-          - amd64
-          - aarch64
-          - arm64
-parts:
-  charm:
-    build-packages: [git]
diff --git a/installers/charm/mysqld-exporter/config.yaml b/installers/charm/mysqld-exporter/config.yaml
deleted file mode 100644 (file)
index 5c0a24b..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-options:
-  ingress_class:
-    type: string
-    description: |
-      Ingress class name. This is useful for selecting the ingress to be used
-      in case there are multiple ingresses in the underlying k8s clusters.
-  ingress_whitelist_source_range:
-    type: string
-    description: |
-      A comma-separated list of CIDRs to store in the
-      ingress.kubernetes.io/whitelist-source-range annotation.
-
-      This can be used to lock down access to
-      Keystone based on source IP address.
-    default: ""
-  tls_secret_name:
-    type: string
-    description: TLS Secret name
-    default: ""
-  site_url:
-    type: string
-    description: Ingress URL
-    default: ""
-  cluster_issuer:
-    type: string
-    description: Name of the cluster issuer for TLS certificates
-    default: ""
-  mysql_uri:
-    type: string
-    description: MySQL URI (external database)
-  image_pull_policy:
-    type: string
-    description: |
-      ImagePullPolicy configuration for the pod.
-      Possible values: always, ifnotpresent, never
-    default: always
-  security_context:
-    description: Enables the security context of the pods
-    type: boolean
-    default: false
diff --git a/installers/charm/mysqld-exporter/metadata.yaml b/installers/charm/mysqld-exporter/metadata.yaml
deleted file mode 100644 (file)
index 7f6fb6e..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-name: mysqld-exporter-k8s
-summary: OSM Prometheus Mysql Exporter
-description: |
-  A CAAS charm to deploy OSM's Prometheus Mysql Exporter.
-series:
-  - kubernetes
-tags:
-  - kubernetes
-  - osm
-  - prometheus
-  - mysql-exporter
-min-juju-version: 2.8.0
-deployment:
-  type: stateless
-  service: cluster
-resources:
-  image:
-    type: oci-image
-    description: Image of mysqld-exporter
-    upstream-source: "bitnami/mysqld-exporter:0.14.0"
-provides:
-  prometheus-scrape:
-    interface: prometheus
-  grafana-dashboard:
-    interface: grafana-dashboard
-requires:
-  mysql:
-    interface: mysql
diff --git a/installers/charm/mysqld-exporter/requirements-test.txt b/installers/charm/mysqld-exporter/requirements-test.txt
deleted file mode 100644 (file)
index 316f6d2..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-
-mock==4.0.3
diff --git a/installers/charm/mysqld-exporter/requirements.txt b/installers/charm/mysqld-exporter/requirements.txt
deleted file mode 100644 (file)
index 8bb93ad..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-git+https://github.com/charmed-osm/ops-lib-charmed-osm/@master
diff --git a/installers/charm/mysqld-exporter/src/charm.py b/installers/charm/mysqld-exporter/src/charm.py
deleted file mode 100755 (executable)
index 153dbfd..0000000
+++ /dev/null
@@ -1,276 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-# pylint: disable=E0213
-
-from ipaddress import ip_network
-import logging
-from pathlib import Path
-from typing import NoReturn, Optional
-from urllib.parse import urlparse
-
-from ops.main import main
-from opslib.osm.charm import CharmedOsmBase, RelationsMissing
-from opslib.osm.interfaces.grafana import GrafanaDashboardTarget
-from opslib.osm.interfaces.mysql import MysqlClient
-from opslib.osm.interfaces.prometheus import PrometheusScrapeTarget
-from opslib.osm.pod import (
-    ContainerV3Builder,
-    IngressResourceV3Builder,
-    PodRestartPolicy,
-    PodSpecV3Builder,
-)
-from opslib.osm.validator import ModelValidator, validator
-
-
-logger = logging.getLogger(__name__)
-
-PORT = 9104
-
-
-class ConfigModel(ModelValidator):
-    site_url: Optional[str]
-    cluster_issuer: Optional[str]
-    ingress_class: Optional[str]
-    ingress_whitelist_source_range: Optional[str]
-    tls_secret_name: Optional[str]
-    mysql_uri: Optional[str]
-    image_pull_policy: str
-    security_context: bool
-
-    @validator("site_url")
-    def validate_site_url(cls, v):
-        if v:
-            parsed = urlparse(v)
-            if not parsed.scheme.startswith("http"):
-                raise ValueError("value must start with http")
-        return v
-
-    @validator("ingress_whitelist_source_range")
-    def validate_ingress_whitelist_source_range(cls, v):
-        if v:
-            ip_network(v)
-        return v
-
-    @validator("mysql_uri")
-    def validate_mysql_uri(cls, v):
-        if v and not v.startswith("mysql://"):
-            raise ValueError("mysql_uri is not properly formed")
-        return v
-
-    @validator("image_pull_policy")
-    def validate_image_pull_policy(cls, v):
-        values = {
-            "always": "Always",
-            "ifnotpresent": "IfNotPresent",
-            "never": "Never",
-        }
-        v = v.lower()
-        if v not in values.keys():
-            raise ValueError("value must be always, ifnotpresent or never")
-        return values[v]
-
-
-class MysqlExporterCharm(CharmedOsmBase):
-    def __init__(self, *args) -> NoReturn:
-        super().__init__(*args, oci_image="image")
-
-        # Provision Kafka relation to exchange information
-        self.mysql_client = MysqlClient(self, "mysql")
-        self.framework.observe(self.on["mysql"].relation_changed, self.configure_pod)
-        self.framework.observe(self.on["mysql"].relation_broken, self.configure_pod)
-
-        # Register relation to provide a Scraping Target
-        self.scrape_target = PrometheusScrapeTarget(self, "prometheus-scrape")
-        self.framework.observe(
-            self.on["prometheus-scrape"].relation_joined, self._publish_scrape_info
-        )
-
-        # Register relation to provide a Dasboard Target
-        self.dashboard_target = GrafanaDashboardTarget(self, "grafana-dashboard")
-        self.framework.observe(
-            self.on["grafana-dashboard"].relation_joined, self._publish_dashboard_info
-        )
-
-    def _publish_scrape_info(self, event) -> NoReturn:
-        """Publishes scraping information for Prometheus.
-
-        Args:
-            event (EventBase): Prometheus relation event.
-        """
-        if self.unit.is_leader():
-            hostname = (
-                urlparse(self.model.config["site_url"]).hostname
-                if self.model.config["site_url"]
-                else self.model.app.name
-            )
-            port = str(PORT)
-            if self.model.config.get("site_url", "").startswith("https://"):
-                port = "443"
-            elif self.model.config.get("site_url", "").startswith("http://"):
-                port = "80"
-
-            self.scrape_target.publish_info(
-                hostname=hostname,
-                port=port,
-                metrics_path="/metrics",
-                scrape_interval="30s",
-                scrape_timeout="15s",
-            )
-
-    def _publish_dashboard_info(self, event) -> NoReturn:
-        """Publish dashboards for Grafana.
-
-        Args:
-            event (EventBase): Grafana relation event.
-        """
-        if self.unit.is_leader():
-            self.dashboard_target.publish_info(
-                name="osm-mysql",
-                dashboard=Path("templates/mysql_exporter_dashboard.json").read_text(),
-            )
-
-    def _check_missing_dependencies(self, config: ConfigModel):
-        """Check if there is any relation missing.
-
-        Args:
-            config (ConfigModel): object with configuration information.
-
-        Raises:
-            RelationsMissing: if kafka is missing.
-        """
-        missing_relations = []
-
-        if not config.mysql_uri and self.mysql_client.is_missing_data_in_unit():
-            missing_relations.append("mysql")
-
-        if missing_relations:
-            raise RelationsMissing(missing_relations)
-
-    def build_pod_spec(self, image_info):
-        """Build the PodSpec to be used.
-
-        Args:
-            image_info (str): container image information.
-
-        Returns:
-            Dict: PodSpec information.
-        """
-        # Validate config
-        config = ConfigModel(**dict(self.config))
-
-        if config.mysql_uri and not self.mysql_client.is_missing_data_in_unit():
-            raise Exception("Mysql data cannot be provided via config and relation")
-
-        # Check relations
-        self._check_missing_dependencies(config)
-
-        data_source = (
-            f'{config.mysql_uri.replace("mysql://", "").replace("@", "@(").split("/")[0]})/'
-            if config.mysql_uri
-            else f"root:{self.mysql_client.root_password}@({self.mysql_client.host}:{self.mysql_client.port})/"
-        )
-
-        # Create Builder for the PodSpec
-        pod_spec_builder = PodSpecV3Builder(
-            enable_security_context=config.security_context
-        )
-
-        # Add secrets to the pod
-        mysql_secret_name = f"{self.app.name}-mysql-secret"
-        pod_spec_builder.add_secret(
-            mysql_secret_name,
-            {"data_source": data_source},
-        )
-
-        # Build container
-        container_builder = ContainerV3Builder(
-            self.app.name,
-            image_info,
-            config.image_pull_policy,
-            run_as_non_root=config.security_context,
-        )
-        container_builder.add_port(name="exporter", port=PORT)
-        container_builder.add_http_readiness_probe(
-            path="/api/health",
-            port=PORT,
-            initial_delay_seconds=10,
-            period_seconds=10,
-            timeout_seconds=5,
-            success_threshold=1,
-            failure_threshold=3,
-        )
-        container_builder.add_http_liveness_probe(
-            path="/api/health",
-            port=PORT,
-            initial_delay_seconds=60,
-            timeout_seconds=30,
-            failure_threshold=10,
-        )
-        container_builder.add_secret_envs(
-            mysql_secret_name, {"DATA_SOURCE_NAME": "data_source"}
-        )
-
-        container = container_builder.build()
-
-        # Add container to PodSpec
-        pod_spec_builder.add_container(container)
-
-        # Add Pod restart policy
-        restart_policy = PodRestartPolicy()
-        restart_policy.add_secrets(secret_names=(mysql_secret_name))
-        pod_spec_builder.set_restart_policy(restart_policy)
-
-        # Add ingress resources to PodSpec if site url exists
-        if config.site_url:
-            parsed = urlparse(config.site_url)
-            annotations = {}
-            if config.ingress_class:
-                annotations["kubernetes.io/ingress.class"] = config.ingress_class
-            ingress_resource_builder = IngressResourceV3Builder(
-                f"{self.app.name}-ingress", annotations
-            )
-
-            if config.ingress_whitelist_source_range:
-                annotations[
-                    "nginx.ingress.kubernetes.io/whitelist-source-range"
-                ] = config.ingress_whitelist_source_range
-
-            if config.cluster_issuer:
-                annotations["cert-manager.io/cluster-issuer"] = config.cluster_issuer
-
-            if parsed.scheme == "https":
-                ingress_resource_builder.add_tls(
-                    [parsed.hostname], config.tls_secret_name
-                )
-            else:
-                annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
-
-            ingress_resource_builder.add_rule(parsed.hostname, self.app.name, PORT)
-            ingress_resource = ingress_resource_builder.build()
-            pod_spec_builder.add_ingress_resource(ingress_resource)
-
-        return pod_spec_builder.build()
-
-
-if __name__ == "__main__":
-    main(MysqlExporterCharm)
diff --git a/installers/charm/mysqld-exporter/src/pod_spec.py b/installers/charm/mysqld-exporter/src/pod_spec.py
deleted file mode 100644 (file)
index 8068be7..0000000
+++ /dev/null
@@ -1,299 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-from ipaddress import ip_network
-import logging
-from typing import Any, Dict, List
-from urllib.parse import urlparse
-
-logger = logging.getLogger(__name__)
-
-
-def _validate_ip_network(network: str) -> bool:
-    """Validate IP network.
-
-    Args:
-        network (str): IP network range.
-
-    Returns:
-        bool: True if valid, false otherwise.
-    """
-    if not network:
-        return True
-
-    try:
-        ip_network(network)
-    except ValueError:
-        return False
-
-    return True
-
-
-def _validate_data(config_data: Dict[str, Any], relation_data: Dict[str, Any]) -> bool:
-    """Validates passed information.
-
-    Args:
-        config_data (Dict[str, Any]): configuration information.
-        relation_data (Dict[str, Any]): relation information
-
-    Raises:
-        ValueError: when config and/or relation data is not valid.
-    """
-    config_validators = {
-        "site_url": lambda value, _: isinstance(value, str)
-        if value is not None
-        else True,
-        "cluster_issuer": lambda value, _: isinstance(value, str)
-        if value is not None
-        else True,
-        "ingress_whitelist_source_range": lambda value, _: _validate_ip_network(value),
-        "tls_secret_name": lambda value, _: isinstance(value, str)
-        if value is not None
-        else True,
-    }
-    relation_validators = {
-        "mysql_host": lambda value, _: isinstance(value, str) and len(value) > 0,
-        "mysql_port": lambda value, _: isinstance(value, str) and int(value) > 0,
-        "mysql_user": lambda value, _: isinstance(value, str) and len(value) > 0,
-        "mysql_password": lambda value, _: isinstance(value, str) and len(value) > 0,
-        "mysql_root_password": lambda value, _: isinstance(value, str)
-        and len(value) > 0,
-    }
-    problems = []
-
-    for key, validator in config_validators.items():
-        valid = validator(config_data.get(key), config_data)
-
-        if not valid:
-            problems.append(key)
-
-    for key, validator in relation_validators.items():
-        valid = validator(relation_data.get(key), relation_data)
-
-        if not valid:
-            problems.append(key)
-
-    if len(problems) > 0:
-        raise ValueError("Errors found in: {}".format(", ".join(problems)))
-
-    return True
-
-
-def _make_pod_ports(port: int) -> List[Dict[str, Any]]:
-    """Generate pod ports details.
-
-    Args:
-        port (int): port to expose.
-
-    Returns:
-        List[Dict[str, Any]]: pod port details.
-    """
-    return [{"name": "mysqld-exporter", "containerPort": port, "protocol": "TCP"}]
-
-
-def _make_pod_envconfig(
-    config: Dict[str, Any], relation_state: Dict[str, Any]
-) -> Dict[str, Any]:
-    """Generate pod environment configuration.
-
-    Args:
-        config (Dict[str, Any]): configuration information.
-        relation_state (Dict[str, Any]): relation state information.
-
-    Returns:
-        Dict[str, Any]: pod environment configuration.
-    """
-    envconfig = {
-        "DATA_SOURCE_NAME": "root:{mysql_root_password}@({mysql_host}:{mysql_port})/".format(
-            **relation_state
-        )
-    }
-
-    return envconfig
-
-
-def _make_pod_ingress_resources(
-    config: Dict[str, Any], app_name: str, port: int
-) -> List[Dict[str, Any]]:
-    """Generate pod ingress resources.
-
-    Args:
-        config (Dict[str, Any]): configuration information.
-        app_name (str): application name.
-        port (int): port to expose.
-
-    Returns:
-        List[Dict[str, Any]]: pod ingress resources.
-    """
-    site_url = config.get("site_url")
-
-    if not site_url:
-        return
-
-    parsed = urlparse(site_url)
-
-    if not parsed.scheme.startswith("http"):
-        return
-
-    ingress_whitelist_source_range = config["ingress_whitelist_source_range"]
-    cluster_issuer = config["cluster_issuer"]
-
-    annotations = {}
-
-    if ingress_whitelist_source_range:
-        annotations[
-            "nginx.ingress.kubernetes.io/whitelist-source-range"
-        ] = ingress_whitelist_source_range
-
-    if cluster_issuer:
-        annotations["cert-manager.io/cluster-issuer"] = cluster_issuer
-
-    ingress_spec_tls = None
-
-    if parsed.scheme == "https":
-        ingress_spec_tls = [{"hosts": [parsed.hostname]}]
-        tls_secret_name = config["tls_secret_name"]
-        if tls_secret_name:
-            ingress_spec_tls[0]["secretName"] = tls_secret_name
-    else:
-        annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
-
-    ingress = {
-        "name": "{}-ingress".format(app_name),
-        "annotations": annotations,
-        "spec": {
-            "rules": [
-                {
-                    "host": parsed.hostname,
-                    "http": {
-                        "paths": [
-                            {
-                                "path": "/",
-                                "backend": {
-                                    "serviceName": app_name,
-                                    "servicePort": port,
-                                },
-                            }
-                        ]
-                    },
-                }
-            ]
-        },
-    }
-    if ingress_spec_tls:
-        ingress["spec"]["tls"] = ingress_spec_tls
-
-    return [ingress]
-
-
-def _make_readiness_probe(port: int) -> Dict[str, Any]:
-    """Generate readiness probe.
-
-    Args:
-        port (int): service port.
-
-    Returns:
-        Dict[str, Any]: readiness probe.
-    """
-    return {
-        "httpGet": {
-            "path": "/api/health",
-            "port": port,
-        },
-        "initialDelaySeconds": 10,
-        "periodSeconds": 10,
-        "timeoutSeconds": 5,
-        "successThreshold": 1,
-        "failureThreshold": 3,
-    }
-
-
-def _make_liveness_probe(port: int) -> Dict[str, Any]:
-    """Generate liveness probe.
-
-    Args:
-        port (int): service port.
-
-    Returns:
-        Dict[str, Any]: liveness probe.
-    """
-    return {
-        "httpGet": {
-            "path": "/api/health",
-            "port": port,
-        },
-        "initialDelaySeconds": 60,
-        "timeoutSeconds": 30,
-        "failureThreshold": 10,
-    }
-
-
-def make_pod_spec(
-    image_info: Dict[str, str],
-    config: Dict[str, Any],
-    relation_state: Dict[str, Any],
-    app_name: str = "mysqld-exporter",
-    port: int = 9104,
-) -> Dict[str, Any]:
-    """Generate the pod spec information.
-
-    Args:
-        image_info (Dict[str, str]): Object provided by
-                                     OCIImageResource("image").fetch().
-        config (Dict[str, Any]): Configuration information.
-        relation_state (Dict[str, Any]): Relation state information.
-        app_name (str, optional): Application name. Defaults to "ro".
-        port (int, optional): Port for the container. Defaults to 9090.
-
-    Returns:
-        Dict[str, Any]: Pod spec dictionary for the charm.
-    """
-    if not image_info:
-        return None
-
-    _validate_data(config, relation_state)
-
-    ports = _make_pod_ports(port)
-    env_config = _make_pod_envconfig(config, relation_state)
-    readiness_probe = _make_readiness_probe(port)
-    liveness_probe = _make_liveness_probe(port)
-    ingress_resources = _make_pod_ingress_resources(config, app_name, port)
-
-    return {
-        "version": 3,
-        "containers": [
-            {
-                "name": app_name,
-                "imageDetails": image_info,
-                "imagePullPolicy": "Always",
-                "ports": ports,
-                "envConfig": env_config,
-                "kubernetes": {
-                    "readinessProbe": readiness_probe,
-                    "livenessProbe": liveness_probe,
-                },
-            }
-        ],
-        "kubernetesResources": {
-            "ingressResources": ingress_resources or [],
-        },
-    }
diff --git a/installers/charm/mysqld-exporter/templates/mysql_exporter_dashboard.json b/installers/charm/mysqld-exporter/templates/mysql_exporter_dashboard.json
deleted file mode 100644 (file)
index 9f9acac..0000000
+++ /dev/null
@@ -1,1145 +0,0 @@
-{
-  "annotations": {
-    "list": [
-      {
-        "builtIn": 1,
-        "datasource": "-- Grafana --",
-        "enable": true,
-        "hide": true,
-        "iconColor": "rgba(0, 211, 255, 1)",
-        "name": "Annotations & Alerts",
-        "type": "dashboard"
-      }
-    ]
-  },
-  "description": "Mysql dashboard",
-  "editable": true,
-  "gnetId": 6239,
-  "graphTooltip": 0,
-  "id": 34,
-  "iteration": 1569307668513,
-  "links": [],
-  "panels": [
-    {
-      "collapsed": false,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 0
-      },
-      "id": 17,
-      "panels": [],
-      "title": "Global status",
-      "type": "row"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": true,
-      "colorValue": false,
-      "colors": [
-        "#bf1b00",
-        "#508642",
-        "#ef843c"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "format": "none",
-      "gauge": {
-        "maxValue": 1,
-        "minValue": 0,
-        "show": false,
-        "thresholdLabels": false,
-        "thresholdMarkers": true
-      },
-      "gridPos": {
-        "h": 7,
-        "w": 6,
-        "x": 0,
-        "y": 1
-      },
-      "id": 11,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "options": {},
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": true,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": true
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "mysql_up{release=\"$release\"}",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": "1,2",
-      "title": "Instance Up",
-      "type": "singlestat",
-      "valueFontSize": "80%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "N/A",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "cacheTimeout": null,
-      "colorBackground": true,
-      "colorValue": false,
-      "colors": [
-        "#d44a3a",
-        "rgba(237, 129, 40, 0.89)",
-        "#508642"
-      ],
-      "datasource": "prometheus - Juju generated source",
-      "format": "s",
-      "gauge": {
-        "maxValue": 100,
-        "minValue": 0,
-        "show": false,
-        "thresholdLabels": false,
-        "thresholdMarkers": true
-      },
-      "gridPos": {
-        "h": 7,
-        "w": 6,
-        "x": 6,
-        "y": 1
-      },
-      "id": 15,
-      "interval": null,
-      "links": [],
-      "mappingType": 1,
-      "mappingTypes": [
-        {
-          "name": "value to text",
-          "value": 1
-        },
-        {
-          "name": "range to text",
-          "value": 2
-        }
-      ],
-      "maxDataPoints": 100,
-      "nullPointMode": "connected",
-      "nullText": null,
-      "options": {},
-      "postfix": "",
-      "postfixFontSize": "50%",
-      "prefix": "",
-      "prefixFontSize": "50%",
-      "rangeMaps": [
-        {
-          "from": "null",
-          "text": "N/A",
-          "to": "null"
-        }
-      ],
-      "sparkline": {
-        "fillColor": "rgba(31, 118, 189, 0.18)",
-        "full": false,
-        "lineColor": "rgb(31, 120, 193)",
-        "show": true
-      },
-      "tableColumn": "",
-      "targets": [
-        {
-          "expr": "mysql_global_status_uptime{release=\"$release\"}",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": "25200,32400",
-      "title": "Uptime",
-      "type": "singlestat",
-      "valueFontSize": "80%",
-      "valueMaps": [
-        {
-          "op": "=",
-          "text": "N/A",
-          "value": "null"
-        }
-      ],
-      "valueName": "current"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 7,
-        "w": 12,
-        "x": 12,
-        "y": 1
-      },
-      "id": 29,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": false,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "mysql_global_status_max_used_connections{release=\"$release\"}",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "current",
-          "refId": "A"
-        },
-        {
-          "expr": "mysql_global_variables_max_connections{release=\"$release\"}",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "Max",
-          "refId": "B"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Mysql Connections",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "collapsed": false,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 8
-      },
-      "id": 19,
-      "panels": [],
-      "title": "I/O",
-      "type": "row"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 9,
-        "w": 12,
-        "x": 0,
-        "y": 9
-      },
-      "id": 5,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [
-        {
-          "alias": "write",
-          "transform": "negative-Y"
-        }
-      ],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "irate(mysql_global_status_innodb_data_reads{release=\"$release\"}[10m])",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "reads",
-          "refId": "A"
-        },
-        {
-          "expr": "irate(mysql_global_status_innodb_data_writes{release=\"$release\"}[10m])",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "write",
-          "refId": "B"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "mysql  disk reads vs writes",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 9,
-        "w": 12,
-        "x": 12,
-        "y": 9
-      },
-      "id": 9,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": false,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [
-        {
-          "alias": "/sent/",
-          "transform": "negative-Y"
-        }
-      ],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "irate(mysql_global_status_bytes_received{release=\"$release\"}[5m])",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "received",
-          "refId": "A"
-        },
-        {
-          "expr": "irate(mysql_global_status_bytes_sent{release=\"$release\"}[5m])",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "sent",
-          "refId": "B"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "mysql network received vs sent",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 7,
-        "w": 12,
-        "x": 0,
-        "y": 18
-      },
-      "id": 2,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": false,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "irate(mysql_global_status_commands_total{release=\"$release\"}[5m]) > 0",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "{{ command }} - {{ release }}",
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Query rates",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 7,
-        "w": 12,
-        "x": 12,
-        "y": 18
-      },
-      "id": 25,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": false,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "mysql_global_status_threads_running{release=\"$release\"} ",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Running Threads",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "decimals": null,
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": "15",
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "collapsed": false,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 25
-      },
-      "id": 21,
-      "panels": [],
-      "title": "Errors",
-      "type": "row"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "description": "The number of connections that were aborted because the client died without closing the connection properly.",
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 9,
-        "w": 12,
-        "x": 0,
-        "y": 26
-      },
-      "id": 13,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": false,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "mysql_global_status_aborted_clients{release=\"$release\"}",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "B"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Aborted clients",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "description": "The number of failed attempts to connect to the MySQL server.",
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 9,
-        "w": 12,
-        "x": 12,
-        "y": 26
-      },
-      "id": 4,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": false,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "mysql_global_status_aborted_connects{release=\"$release\"}",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "",
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "mysql aborted Connects",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "collapsed": false,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 35
-      },
-      "id": 23,
-      "panels": [],
-      "title": "Disk usage",
-      "type": "row"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 9,
-        "w": 12,
-        "x": 0,
-        "y": 36
-      },
-      "id": 27,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "sum(mysql_info_schema_table_size{component=\"data_length\",release=\"$release\"})",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "Tables",
-          "refId": "A"
-        },
-        {
-          "expr": "sum(mysql_info_schema_table_size{component=\"index_length\",release=\"$release\"})",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "legendFormat": "Indexes",
-          "refId": "B"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Disk usage tables / indexes",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "decbytes",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "prometheus - Juju generated source",
-      "fill": 1,
-      "fillGradient": 0,
-      "gridPos": {
-        "h": 9,
-        "w": 12,
-        "x": 12,
-        "y": 36
-      },
-      "id": 7,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": false,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "links": [],
-      "nullPointMode": "null",
-      "options": {
-        "dataLinks": []
-      },
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "sum(mysql_info_schema_table_rows{release=\"$release\"})",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeRegions": [],
-      "timeShift": null,
-      "title": "Sum of all rows",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "decimals": null,
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    }
-  ],
-  "schemaVersion": 19,
-  "style": "dark",
-  "tags": [
-  ],
-  "templating": {
-    "list": [
-      {
-        "allValue": null,
-        "current": {
-          "isNone": true,
-          "text": "None",
-          "value": ""
-        },
-        "datasource": "prometheus - Juju generated source",
-        "definition": "",
-        "hide": 0,
-        "includeAll": false,
-        "label": null,
-        "multi": false,
-        "name": "release",
-        "options": [],
-        "query": "label_values(mysql_up,release)",
-        "refresh": 1,
-        "regex": "",
-        "skipUrlSync": false,
-        "sort": 0,
-        "tagValuesQuery": "",
-        "tags": [],
-        "tagsQuery": "",
-        "type": "query",
-        "useTags": false
-      }
-    ]
-  },
-  "time": {
-    "from": "now-1h",
-    "to": "now"
-  },
-  "timepicker": {
-    "refresh_intervals": [
-      "5s",
-      "10s",
-      "30s",
-      "1m",
-      "5m",
-      "15m",
-      "30m",
-      "1h",
-      "2h",
-      "1d"
-    ],
-    "time_options": [
-      "5m",
-      "15m",
-      "1h",
-      "6h",
-      "12h",
-      "24h",
-      "2d",
-      "7d",
-      "30d"
-    ]
-  },
-  "timezone": "",
-  "title": "Mysql",
-  "uid": "6-kPlS7ik",
-  "version": 1
-}
diff --git a/installers/charm/mysqld-exporter/tests/__init__.py b/installers/charm/mysqld-exporter/tests/__init__.py
deleted file mode 100644 (file)
index 90dc417..0000000
+++ /dev/null
@@ -1,39 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-"""Init mocking for unit tests."""
-
-import sys
-
-import mock
-
-
-class OCIImageResourceErrorMock(Exception):
-    pass
-
-
-sys.path.append("src")
-
-oci_image = mock.MagicMock()
-oci_image.OCIImageResourceError = OCIImageResourceErrorMock
-sys.modules["oci_image"] = oci_image
-sys.modules["oci_image"].OCIImageResource().fetch.return_value = {}
diff --git a/installers/charm/mysqld-exporter/tests/test_charm.py b/installers/charm/mysqld-exporter/tests/test_charm.py
deleted file mode 100644 (file)
index ddaacaf..0000000
+++ /dev/null
@@ -1,595 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-import sys
-from typing import NoReturn
-import unittest
-
-from charm import MysqlExporterCharm
-from ops.model import ActiveStatus, BlockedStatus
-from ops.testing import Harness
-
-
-class TestCharm(unittest.TestCase):
-    """Mysql Exporter Charm unit tests."""
-
-    def setUp(self) -> NoReturn:
-        """Test setup"""
-        self.image_info = sys.modules["oci_image"].OCIImageResource().fetch()
-        self.harness = Harness(MysqlExporterCharm)
-        self.harness.set_leader(is_leader=True)
-        self.harness.begin()
-        self.config = {
-            "ingress_whitelist_source_range": "",
-            "tls_secret_name": "",
-            "site_url": "https://mysql-exporter.192.168.100.100.nip.io",
-            "cluster_issuer": "vault-issuer",
-        }
-        self.harness.update_config(self.config)
-
-    def test_config_changed_no_relations(
-        self,
-    ) -> NoReturn:
-        """Test ingress resources without HTTP."""
-
-        self.harness.charm.on.config_changed.emit()
-
-        # Assertions
-        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
-        print(self.harness.charm.unit.status.message)
-        self.assertTrue(
-            all(
-                relation in self.harness.charm.unit.status.message
-                for relation in ["mysql"]
-            )
-        )
-
-    def test_config_changed_non_leader(
-        self,
-    ) -> NoReturn:
-        """Test ingress resources without HTTP."""
-        self.harness.set_leader(is_leader=False)
-        self.harness.charm.on.config_changed.emit()
-
-        # Assertions
-        self.assertIsInstance(self.harness.charm.unit.status, ActiveStatus)
-
-    def test_with_relations(
-        self,
-    ) -> NoReturn:
-        "Test with relations"
-        self.initialize_mysql_relation()
-
-        # Verifying status
-        self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
-
-    def test_with_config(
-        self,
-    ) -> NoReturn:
-        "Test with config"
-        self.initialize_mysql_relation()
-
-        # Verifying status
-        self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
-
-    def test_mysql_exception_relation_and_config(
-        self,
-    ) -> NoReturn:
-        self.initialize_mysql_config()
-        self.initialize_mysql_relation()
-
-        # Verifying status
-        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
-
-    def initialize_mysql_relation(self):
-        mongodb_relation_id = self.harness.add_relation("mysql", "mysql")
-        self.harness.add_relation_unit(mongodb_relation_id, "mysql/0")
-        self.harness.update_relation_data(
-            mongodb_relation_id,
-            "mysql/0",
-            {
-                "user": "user",
-                "password": "pass",
-                "host": "host",
-                "port": "1234",
-                "database": "pol",
-                "root_password": "root_password",
-            },
-        )
-
-    def initialize_mysql_config(self):
-        self.harness.update_config({"mysql_uri": "mysql://user:pass@mysql-host:3306"})
-
-
-if __name__ == "__main__":
-    unittest.main()
-
-
-# class TestCharm(unittest.TestCase):
-#     """Mysql Exporter Charm unit tests."""
-#
-#     def setUp(self) -> NoReturn:
-#         """Test setup"""
-#         self.harness = Harness(MysqldExporterCharm)
-#         self.harness.set_leader(is_leader=True)
-#         self.harness.begin()
-#
-#     def test_on_start_without_relations(self) -> NoReturn:
-#         """Test installation without any relation."""
-#         self.harness.charm.on.start.emit()
-#
-#         # Verifying status
-#         self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
-#
-#         # Verifying status message
-#         self.assertGreater(len(self.harness.charm.unit.status.message), 0)
-#         self.assertTrue(
-#             self.harness.charm.unit.status.message.startswith("Waiting for ")
-#         )
-#         self.assertIn("mysql", self.harness.charm.unit.status.message)
-#         self.assertTrue(self.harness.charm.unit.status.message.endswith(" relation"))
-#
-#     def test_on_start_with_relations_without_http(self) -> NoReturn:
-#         """Test deployment."""
-#         expected_result = {
-#             "version": 3,
-#             "containers": [
-#                 {
-#                     "name": "mysqld-exporter",
-#                     "imageDetails": self.harness.charm.image.fetch(),
-#                     "imagePullPolicy": "Always",
-#                     "ports": [
-#                         {
-#                             "name": "mysqld-exporter",
-#                             "containerPort": 9104,
-#                             "protocol": "TCP",
-#                         }
-#                     ],
-#                     "envConfig": {"DATA_SOURCE_NAME": "root:rootpw@(mysql:3306)/"},
-#                     "kubernetes": {
-#                         "readinessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": 9104,
-#                             },
-#                             "initialDelaySeconds": 10,
-#                             "periodSeconds": 10,
-#                             "timeoutSeconds": 5,
-#                             "successThreshold": 1,
-#                             "failureThreshold": 3,
-#                         },
-#                         "livenessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": 9104,
-#                             },
-#                             "initialDelaySeconds": 60,
-#                             "timeoutSeconds": 30,
-#                             "failureThreshold": 10,
-#                         },
-#                     },
-#                 },
-#             ],
-#             "kubernetesResources": {"ingressResources": []},
-#         }
-#
-#         self.harness.charm.on.start.emit()
-#
-#         # Initializing the mysql relation
-#         relation_id = self.harness.add_relation("mysql", "mysql")
-#         self.harness.add_relation_unit(relation_id, "mysql/0")
-#         self.harness.update_relation_data(
-#             relation_id,
-#             "mysql/0",
-#             {
-#                 "host": "mysql",
-#                 "port": "3306",
-#                 "user": "mano",
-#                 "password": "manopw",
-#                 "root_password": "rootpw",
-#             },
-#         )
-#
-#         # Verifying status
-#         self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
-#
-#         pod_spec, _ = self.harness.get_pod_spec()
-#
-#         self.assertDictEqual(expected_result, pod_spec)
-#
-#     def test_ingress_resources_with_http(self) -> NoReturn:
-#         """Test ingress resources with HTTP."""
-#         expected_result = {
-#             "version": 3,
-#             "containers": [
-#                 {
-#                     "name": "mysqld-exporter",
-#                     "imageDetails": self.harness.charm.image.fetch(),
-#                     "imagePullPolicy": "Always",
-#                     "ports": [
-#                         {
-#                             "name": "mysqld-exporter",
-#                             "containerPort": 9104,
-#                             "protocol": "TCP",
-#                         }
-#                     ],
-#                     "envConfig": {"DATA_SOURCE_NAME": "root:rootpw@(mysql:3306)/"},
-#                     "kubernetes": {
-#                         "readinessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": 9104,
-#                             },
-#                             "initialDelaySeconds": 10,
-#                             "periodSeconds": 10,
-#                             "timeoutSeconds": 5,
-#                             "successThreshold": 1,
-#                             "failureThreshold": 3,
-#                         },
-#                         "livenessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": 9104,
-#                             },
-#                             "initialDelaySeconds": 60,
-#                             "timeoutSeconds": 30,
-#                             "failureThreshold": 10,
-#                         },
-#                     },
-#                 },
-#             ],
-#             "kubernetesResources": {
-#                 "ingressResources": [
-#                     {
-#                         "name": "mysqld-exporter-ingress",
-#                         "annotations": {
-#                             "nginx.ingress.kubernetes.io/ssl-redirect": "false",
-#                         },
-#                         "spec": {
-#                             "rules": [
-#                                 {
-#                                     "host": "mysqld-exporter",
-#                                     "http": {
-#                                         "paths": [
-#                                             {
-#                                                 "path": "/",
-#                                                 "backend": {
-#                                                     "serviceName": "mysqld-exporter",
-#                                                     "servicePort": 9104,
-#                                                 },
-#                                             }
-#                                         ]
-#                                     },
-#                                 }
-#                             ]
-#                         },
-#                     }
-#                 ],
-#             },
-#         }
-#
-#         self.harness.charm.on.start.emit()
-#
-#         # Initializing the mysql relation
-#         relation_id = self.harness.add_relation("mysql", "mysql")
-#         self.harness.add_relation_unit(relation_id, "mysql/0")
-#         self.harness.update_relation_data(
-#             relation_id,
-#             "mysql/0",
-#             {
-#                 "host": "mysql",
-#                 "port": "3306",
-#                 "user": "mano",
-#                 "password": "manopw",
-#                 "root_password": "rootpw",
-#             },
-#         )
-#
-#         self.harness.update_config({"site_url": "http://mysqld-exporter"})
-#
-#         pod_spec, _ = self.harness.get_pod_spec()
-#
-#         self.assertDictEqual(expected_result, pod_spec)
-#
-#     def test_ingress_resources_with_https(self) -> NoReturn:
-#         """Test ingress resources with HTTPS."""
-#         expected_result = {
-#             "version": 3,
-#             "containers": [
-#                 {
-#                     "name": "mysqld-exporter",
-#                     "imageDetails": self.harness.charm.image.fetch(),
-#                     "imagePullPolicy": "Always",
-#                     "ports": [
-#                         {
-#                             "name": "mysqld-exporter",
-#                             "containerPort": 9104,
-#                             "protocol": "TCP",
-#                         }
-#                     ],
-#                     "envConfig": {"DATA_SOURCE_NAME": "root:rootpw@(mysql:3306)/"},
-#                     "kubernetes": {
-#                         "readinessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": 9104,
-#                             },
-#                             "initialDelaySeconds": 10,
-#                             "periodSeconds": 10,
-#                             "timeoutSeconds": 5,
-#                             "successThreshold": 1,
-#                             "failureThreshold": 3,
-#                         },
-#                         "livenessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": 9104,
-#                             },
-#                             "initialDelaySeconds": 60,
-#                             "timeoutSeconds": 30,
-#                             "failureThreshold": 10,
-#                         },
-#                     },
-#                 },
-#             ],
-#             "kubernetesResources": {
-#                 "ingressResources": [
-#                     {
-#                         "name": "mysqld-exporter-ingress",
-#                         "annotations": {},
-#                         "spec": {
-#                             "rules": [
-#                                 {
-#                                     "host": "mysqld-exporter",
-#                                     "http": {
-#                                         "paths": [
-#                                             {
-#                                                 "path": "/",
-#                                                 "backend": {
-#                                                     "serviceName": "mysqld-exporter",
-#                                                     "servicePort": 9104,
-#                                                 },
-#                                             }
-#                                         ]
-#                                     },
-#                                 }
-#                             ],
-#                             "tls": [
-#                                 {
-#                                     "hosts": ["mysqld-exporter"],
-#                                     "secretName": "mysqld-exporter",
-#                                 }
-#                             ],
-#                         },
-#                     }
-#                 ],
-#             },
-#         }
-#
-#         self.harness.charm.on.start.emit()
-#
-#         # Initializing the mysql relation
-#         relation_id = self.harness.add_relation("mysql", "mysql")
-#         self.harness.add_relation_unit(relation_id, "mysql/0")
-#         self.harness.update_relation_data(
-#             relation_id,
-#             "mysql/0",
-#             {
-#                 "host": "mysql",
-#                 "port": "3306",
-#                 "user": "mano",
-#                 "password": "manopw",
-#                 "root_password": "rootpw",
-#             },
-#         )
-#
-#         self.harness.update_config(
-#             {
-#                 "site_url": "https://mysqld-exporter",
-#                 "tls_secret_name": "mysqld-exporter",
-#             }
-#         )
-#
-#         pod_spec, _ = self.harness.get_pod_spec()
-#
-#         self.assertDictEqual(expected_result, pod_spec)
-#
-#     def test_ingress_resources_with_https_and_ingress_whitelist(self) -> NoReturn:
-#         """Test ingress resources with HTTPS and ingress whitelist."""
-#         expected_result = {
-#             "version": 3,
-#             "containers": [
-#                 {
-#                     "name": "mysqld-exporter",
-#                     "imageDetails": self.harness.charm.image.fetch(),
-#                     "imagePullPolicy": "Always",
-#                     "ports": [
-#                         {
-#                             "name": "mysqld-exporter",
-#                             "containerPort": 9104,
-#                             "protocol": "TCP",
-#                         }
-#                     ],
-#                     "envConfig": {"DATA_SOURCE_NAME": "root:rootpw@(mysql:3306)/"},
-#                     "kubernetes": {
-#                         "readinessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": 9104,
-#                             },
-#                             "initialDelaySeconds": 10,
-#                             "periodSeconds": 10,
-#                             "timeoutSeconds": 5,
-#                             "successThreshold": 1,
-#                             "failureThreshold": 3,
-#                         },
-#                         "livenessProbe": {
-#                             "httpGet": {
-#                                 "path": "/api/health",
-#                                 "port": 9104,
-#                             },
-#                             "initialDelaySeconds": 60,
-#                             "timeoutSeconds": 30,
-#                             "failureThreshold": 10,
-#                         },
-#                     },
-#                 },
-#             ],
-#             "kubernetesResources": {
-#                 "ingressResources": [
-#                     {
-#                         "name": "mysqld-exporter-ingress",
-#                         "annotations": {
-#                             "nginx.ingress.kubernetes.io/whitelist-source-range": "0.0.0.0/0",
-#                         },
-#                         "spec": {
-#                             "rules": [
-#                                 {
-#                                     "host": "mysqld-exporter",
-#                                     "http": {
-#                                         "paths": [
-#                                             {
-#                                                 "path": "/",
-#                                                 "backend": {
-#                                                     "serviceName": "mysqld-exporter",
-#                                                     "servicePort": 9104,
-#                                                 },
-#                                             }
-#                                         ]
-#                                     },
-#                                 }
-#                             ],
-#                             "tls": [
-#                                 {
-#                                     "hosts": ["mysqld-exporter"],
-#                                     "secretName": "mysqld-exporter",
-#                                 }
-#                             ],
-#                         },
-#                     }
-#                 ],
-#             },
-#         }
-#
-#         self.harness.charm.on.start.emit()
-#
-#         # Initializing the mysql relation
-#         relation_id = self.harness.add_relation("mysql", "mysql")
-#         self.harness.add_relation_unit(relation_id, "mysql/0")
-#         self.harness.update_relation_data(
-#             relation_id,
-#             "mysql/0",
-#             {
-#                 "host": "mysql",
-#                 "port": "3306",
-#                 "user": "mano",
-#                 "password": "manopw",
-#                 "root_password": "rootpw",
-#             },
-#         )
-#
-#         self.harness.update_config(
-#             {
-#                 "site_url": "https://mysqld-exporter",
-#                 "tls_secret_name": "mysqld-exporter",
-#                 "ingress_whitelist_source_range": "0.0.0.0/0",
-#             }
-#         )
-#
-#         pod_spec, _ = self.harness.get_pod_spec()
-#
-#         self.assertDictEqual(expected_result, pod_spec)
-#
-#     def test_on_mysql_unit_relation_changed(self) -> NoReturn:
-#         """Test to see if mysql relation is updated."""
-#         self.harness.charm.on.start.emit()
-#
-#         relation_id = self.harness.add_relation("mysql", "mysql")
-#         self.harness.add_relation_unit(relation_id, "mysql/0")
-#         self.harness.update_relation_data(
-#             relation_id,
-#             "mysql/0",
-#             {
-#                 "host": "mysql",
-#                 "port": "3306",
-#                 "user": "mano",
-#                 "password": "manopw",
-#                 "root_password": "rootpw",
-#             },
-#         )
-#
-#         # Verifying status
-#         self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
-#
-#     def test_publish_target_info(self) -> NoReturn:
-#         """Test to see if target relation is updated."""
-#         expected_result = {
-#             "hostname": "mysqld-exporter",
-#             "port": "9104",
-#             "metrics_path": "/metrics",
-#             "scrape_interval": "30s",
-#             "scrape_timeout": "15s",
-#         }
-#
-#         self.harness.charm.on.start.emit()
-#
-#         relation_id = self.harness.add_relation("prometheus-scrape", "prometheus")
-#         self.harness.add_relation_unit(relation_id, "prometheus/0")
-#         relation_data = self.harness.get_relation_data(relation_id, "mysqld-exporter/0")
-#
-#         self.assertDictEqual(expected_result, relation_data)
-#
-#     def test_publish_scrape_info_with_site_url(self) -> NoReturn:
-#         """Test to see if target relation is updated."""
-#         expected_result = {
-#             "hostname": "mysqld-exporter-osm",
-#             "port": "80",
-#             "metrics_path": "/metrics",
-#             "scrape_interval": "30s",
-#             "scrape_timeout": "15s",
-#         }
-#
-#         self.harness.charm.on.start.emit()
-#
-#         self.harness.update_config({"site_url": "http://mysqld-exporter-osm"})
-#
-#         relation_id = self.harness.add_relation("prometheus-scrape", "prometheus")
-#         self.harness.add_relation_unit(relation_id, "prometheus/0")
-#         relation_data = self.harness.get_relation_data(relation_id, "mysqld-exporter/0")
-#
-#         self.assertDictEqual(expected_result, relation_data)
-#
-#     def test_publish_dashboard_info(self) -> NoReturn:
-#         """Test to see if dashboard relation is updated."""
-#         self.harness.charm.on.start.emit()
-#
-#         relation_id = self.harness.add_relation("grafana-dashboard", "grafana")
-#         self.harness.add_relation_unit(relation_id, "grafana/0")
-#         relation_data = self.harness.get_relation_data(relation_id, "mysqld-exporter/0")
-#
-#         self.assertTrue("dashboard" in relation_data)
-#         self.assertTrue(len(relation_data["dashboard"]) > 0)
-#         self.assertEqual(relation_data["name"], "osm-mysql")
-#
-#
-# if __name__ == "__main__":
-#     unittest.main()
diff --git a/installers/charm/mysqld-exporter/tests/test_pod_spec.py b/installers/charm/mysqld-exporter/tests/test_pod_spec.py
deleted file mode 100644 (file)
index a9c29ef..0000000
+++ /dev/null
@@ -1,513 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-from typing import NoReturn
-import unittest
-
-import pod_spec
-
-
-class TestPodSpec(unittest.TestCase):
-    """Pod spec unit tests."""
-
-    def test_make_pod_ports(self) -> NoReturn:
-        """Testing make pod ports."""
-        port = 9104
-
-        expected_result = [
-            {
-                "name": "mysqld-exporter",
-                "containerPort": port,
-                "protocol": "TCP",
-            }
-        ]
-
-        pod_ports = pod_spec._make_pod_ports(port)
-
-        self.assertListEqual(expected_result, pod_ports)
-
-    def test_make_pod_envconfig(self) -> NoReturn:
-        """Teting make pod envconfig."""
-        config = {}
-        relation_state = {
-            "mysql_host": "mysql",
-            "mysql_port": "3306",
-            "mysql_user": "mano",
-            "mysql_password": "manopw",
-            "mysql_root_password": "rootpw",
-        }
-
-        expected_result = {
-            "DATA_SOURCE_NAME": "root:{mysql_root_password}@({mysql_host}:{mysql_port})/".format(
-                **relation_state
-            )
-        }
-
-        pod_envconfig = pod_spec._make_pod_envconfig(config, relation_state)
-
-        self.assertDictEqual(expected_result, pod_envconfig)
-
-    def test_make_pod_ingress_resources_without_site_url(self) -> NoReturn:
-        """Testing make pod ingress resources without site_url."""
-        config = {
-            "site_url": "",
-            "cluster_issuer": "",
-        }
-        app_name = "mysqld-exporter"
-        port = 9104
-
-        pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-            config, app_name, port
-        )
-
-        self.assertIsNone(pod_ingress_resources)
-
-    def test_make_pod_ingress_resources(self) -> NoReturn:
-        """Testing make pod ingress resources."""
-        config = {
-            "site_url": "http://mysqld-exporter",
-            "cluster_issuer": "",
-            "ingress_whitelist_source_range": "",
-        }
-        app_name = "mysqld-exporter"
-        port = 9104
-
-        expected_result = [
-            {
-                "name": f"{app_name}-ingress",
-                "annotations": {
-                    "nginx.ingress.kubernetes.io/ssl-redirect": "false",
-                },
-                "spec": {
-                    "rules": [
-                        {
-                            "host": app_name,
-                            "http": {
-                                "paths": [
-                                    {
-                                        "path": "/",
-                                        "backend": {
-                                            "serviceName": app_name,
-                                            "servicePort": port,
-                                        },
-                                    }
-                                ]
-                            },
-                        }
-                    ]
-                },
-            }
-        ]
-
-        pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-            config, app_name, port
-        )
-
-        self.assertListEqual(expected_result, pod_ingress_resources)
-
-    def test_make_pod_ingress_resources_with_whitelist_source_range(self) -> NoReturn:
-        """Testing make pod ingress resources with whitelist_source_range."""
-        config = {
-            "site_url": "http://mysqld-exporter",
-            "cluster_issuer": "",
-            "ingress_whitelist_source_range": "0.0.0.0/0",
-        }
-        app_name = "mysqld-exporter"
-        port = 9104
-
-        expected_result = [
-            {
-                "name": f"{app_name}-ingress",
-                "annotations": {
-                    "nginx.ingress.kubernetes.io/ssl-redirect": "false",
-                    "nginx.ingress.kubernetes.io/whitelist-source-range": config[
-                        "ingress_whitelist_source_range"
-                    ],
-                },
-                "spec": {
-                    "rules": [
-                        {
-                            "host": app_name,
-                            "http": {
-                                "paths": [
-                                    {
-                                        "path": "/",
-                                        "backend": {
-                                            "serviceName": app_name,
-                                            "servicePort": port,
-                                        },
-                                    }
-                                ]
-                            },
-                        }
-                    ]
-                },
-            }
-        ]
-
-        pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-            config, app_name, port
-        )
-
-        self.assertListEqual(expected_result, pod_ingress_resources)
-
-    def test_make_pod_ingress_resources_with_https(self) -> NoReturn:
-        """Testing make pod ingress resources with HTTPs."""
-        config = {
-            "site_url": "https://mysqld-exporter",
-            "cluster_issuer": "",
-            "ingress_whitelist_source_range": "",
-            "tls_secret_name": "",
-        }
-        app_name = "mysqld-exporter"
-        port = 9104
-
-        expected_result = [
-            {
-                "name": f"{app_name}-ingress",
-                "annotations": {},
-                "spec": {
-                    "rules": [
-                        {
-                            "host": app_name,
-                            "http": {
-                                "paths": [
-                                    {
-                                        "path": "/",
-                                        "backend": {
-                                            "serviceName": app_name,
-                                            "servicePort": port,
-                                        },
-                                    }
-                                ]
-                            },
-                        }
-                    ],
-                    "tls": [{"hosts": [app_name]}],
-                },
-            }
-        ]
-
-        pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-            config, app_name, port
-        )
-
-        self.assertListEqual(expected_result, pod_ingress_resources)
-
-    def test_make_pod_ingress_resources_with_https_tls_secret_name(self) -> NoReturn:
-        """Testing make pod ingress resources with HTTPs and TLS secret name."""
-        config = {
-            "site_url": "https://mysqld-exporter",
-            "cluster_issuer": "",
-            "ingress_whitelist_source_range": "",
-            "tls_secret_name": "secret_name",
-        }
-        app_name = "mysqld-exporter"
-        port = 9104
-
-        expected_result = [
-            {
-                "name": f"{app_name}-ingress",
-                "annotations": {},
-                "spec": {
-                    "rules": [
-                        {
-                            "host": app_name,
-                            "http": {
-                                "paths": [
-                                    {
-                                        "path": "/",
-                                        "backend": {
-                                            "serviceName": app_name,
-                                            "servicePort": port,
-                                        },
-                                    }
-                                ]
-                            },
-                        }
-                    ],
-                    "tls": [
-                        {"hosts": [app_name], "secretName": config["tls_secret_name"]}
-                    ],
-                },
-            }
-        ]
-
-        pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-            config, app_name, port
-        )
-
-        self.assertListEqual(expected_result, pod_ingress_resources)
-
-    def test_make_readiness_probe(self) -> NoReturn:
-        """Testing make readiness probe."""
-        port = 9104
-
-        expected_result = {
-            "httpGet": {
-                "path": "/api/health",
-                "port": port,
-            },
-            "initialDelaySeconds": 10,
-            "periodSeconds": 10,
-            "timeoutSeconds": 5,
-            "successThreshold": 1,
-            "failureThreshold": 3,
-        }
-
-        readiness_probe = pod_spec._make_readiness_probe(port)
-
-        self.assertDictEqual(expected_result, readiness_probe)
-
-    def test_make_liveness_probe(self) -> NoReturn:
-        """Testing make liveness probe."""
-        port = 9104
-
-        expected_result = {
-            "httpGet": {
-                "path": "/api/health",
-                "port": port,
-            },
-            "initialDelaySeconds": 60,
-            "timeoutSeconds": 30,
-            "failureThreshold": 10,
-        }
-
-        liveness_probe = pod_spec._make_liveness_probe(port)
-
-        self.assertDictEqual(expected_result, liveness_probe)
-
-    def test_make_pod_spec(self) -> NoReturn:
-        """Testing make pod spec."""
-        image_info = {"upstream-source": "bitnami/mysqld-exporter:latest"}
-        config = {
-            "site_url": "",
-            "cluster_issuer": "",
-        }
-        relation_state = {
-            "mysql_host": "mysql",
-            "mysql_port": "3306",
-            "mysql_user": "mano",
-            "mysql_password": "manopw",
-            "mysql_root_password": "rootpw",
-        }
-        app_name = "mysqld-exporter"
-        port = 9104
-
-        expected_result = {
-            "version": 3,
-            "containers": [
-                {
-                    "name": app_name,
-                    "imageDetails": image_info,
-                    "imagePullPolicy": "Always",
-                    "ports": [
-                        {
-                            "name": app_name,
-                            "containerPort": port,
-                            "protocol": "TCP",
-                        }
-                    ],
-                    "envConfig": {
-                        "DATA_SOURCE_NAME": "root:{mysql_root_password}@({mysql_host}:{mysql_port})/".format(
-                            **relation_state
-                        )
-                    },
-                    "kubernetes": {
-                        "readinessProbe": {
-                            "httpGet": {
-                                "path": "/api/health",
-                                "port": port,
-                            },
-                            "initialDelaySeconds": 10,
-                            "periodSeconds": 10,
-                            "timeoutSeconds": 5,
-                            "successThreshold": 1,
-                            "failureThreshold": 3,
-                        },
-                        "livenessProbe": {
-                            "httpGet": {
-                                "path": "/api/health",
-                                "port": port,
-                            },
-                            "initialDelaySeconds": 60,
-                            "timeoutSeconds": 30,
-                            "failureThreshold": 10,
-                        },
-                    },
-                }
-            ],
-            "kubernetesResources": {"ingressResources": []},
-        }
-
-        spec = pod_spec.make_pod_spec(
-            image_info, config, relation_state, app_name, port
-        )
-
-        self.assertDictEqual(expected_result, spec)
-
-    def test_make_pod_spec_with_ingress(self) -> NoReturn:
-        """Testing make pod spec."""
-        image_info = {"upstream-source": "bitnami/mysqld-exporter:latest"}
-        config = {
-            "site_url": "https://mysqld-exporter",
-            "cluster_issuer": "",
-            "tls_secret_name": "mysqld-exporter",
-            "ingress_whitelist_source_range": "0.0.0.0/0",
-        }
-        relation_state = {
-            "mysql_host": "mysql",
-            "mysql_port": "3306",
-            "mysql_user": "mano",
-            "mysql_password": "manopw",
-            "mysql_root_password": "rootpw",
-        }
-        app_name = "mysqld-exporter"
-        port = 9104
-
-        expected_result = {
-            "version": 3,
-            "containers": [
-                {
-                    "name": app_name,
-                    "imageDetails": image_info,
-                    "imagePullPolicy": "Always",
-                    "ports": [
-                        {
-                            "name": app_name,
-                            "containerPort": port,
-                            "protocol": "TCP",
-                        }
-                    ],
-                    "envConfig": {
-                        "DATA_SOURCE_NAME": "root:{mysql_root_password}@({mysql_host}:{mysql_port})/".format(
-                            **relation_state
-                        )
-                    },
-                    "kubernetes": {
-                        "readinessProbe": {
-                            "httpGet": {
-                                "path": "/api/health",
-                                "port": port,
-                            },
-                            "initialDelaySeconds": 10,
-                            "periodSeconds": 10,
-                            "timeoutSeconds": 5,
-                            "successThreshold": 1,
-                            "failureThreshold": 3,
-                        },
-                        "livenessProbe": {
-                            "httpGet": {
-                                "path": "/api/health",
-                                "port": port,
-                            },
-                            "initialDelaySeconds": 60,
-                            "timeoutSeconds": 30,
-                            "failureThreshold": 10,
-                        },
-                    },
-                }
-            ],
-            "kubernetesResources": {
-                "ingressResources": [
-                    {
-                        "name": "{}-ingress".format(app_name),
-                        "annotations": {
-                            "nginx.ingress.kubernetes.io/whitelist-source-range": config.get(
-                                "ingress_whitelist_source_range"
-                            ),
-                        },
-                        "spec": {
-                            "rules": [
-                                {
-                                    "host": app_name,
-                                    "http": {
-                                        "paths": [
-                                            {
-                                                "path": "/",
-                                                "backend": {
-                                                    "serviceName": app_name,
-                                                    "servicePort": port,
-                                                },
-                                            }
-                                        ]
-                                    },
-                                }
-                            ],
-                            "tls": [
-                                {
-                                    "hosts": [app_name],
-                                    "secretName": config.get("tls_secret_name"),
-                                }
-                            ],
-                        },
-                    }
-                ],
-            },
-        }
-
-        spec = pod_spec.make_pod_spec(
-            image_info, config, relation_state, app_name, port
-        )
-
-        self.assertDictEqual(expected_result, spec)
-
-    def test_make_pod_spec_without_image_info(self) -> NoReturn:
-        """Testing make pod spec without image_info."""
-        image_info = None
-        config = {
-            "site_url": "",
-            "cluster_issuer": "",
-        }
-        relation_state = {
-            "mysql_host": "mysql",
-            "mysql_port": 3306,
-            "mysql_user": "mano",
-            "mysql_password": "manopw",
-            "mysql_root_password": "rootpw",
-        }
-        app_name = "mysqld-exporter"
-        port = 9104
-
-        spec = pod_spec.make_pod_spec(
-            image_info, config, relation_state, app_name, port
-        )
-
-        self.assertIsNone(spec)
-
-    def test_make_pod_spec_without_relation_state(self) -> NoReturn:
-        """Testing make pod spec without relation_state."""
-        image_info = {"upstream-source": "bitnami/mysqld-exporter:latest"}
-        config = {
-            "site_url": "",
-            "cluster_issuer": "",
-        }
-        relation_state = {}
-        app_name = "mysqld-exporter"
-        port = 9104
-
-        with self.assertRaises(ValueError):
-            pod_spec.make_pod_spec(image_info, config, relation_state, app_name, port)
-
-
-if __name__ == "__main__":
-    unittest.main()
diff --git a/installers/charm/mysqld-exporter/tox.ini b/installers/charm/mysqld-exporter/tox.ini
deleted file mode 100644 (file)
index 4c7970d..0000000
+++ /dev/null
@@ -1,126 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-#######################################################################################
-
-[tox]
-envlist = black, cover, flake8, pylint, yamllint, safety
-skipsdist = true
-
-[tox:jenkins]
-toxworkdir = /tmp/.tox
-
-[testenv]
-basepython = python3.8
-setenv = VIRTUAL_ENV={envdir}
-         PYTHONDONTWRITEBYTECODE = 1
-deps =  -r{toxinidir}/requirements.txt
-
-
-#######################################################################################
-[testenv:black]
-deps = black
-commands =
-        black --check --diff src/ tests/
-
-
-#######################################################################################
-[testenv:cover]
-deps =  {[testenv]deps}
-        -r{toxinidir}/requirements-test.txt
-        coverage
-        nose2
-commands =
-        sh -c 'rm -f nosetests.xml'
-        coverage erase
-        nose2 -C --coverage src
-        coverage report --omit='*tests*'
-        coverage html -d ./cover --omit='*tests*'
-        coverage xml -o coverage.xml --omit=*tests*
-whitelist_externals = sh
-
-
-#######################################################################################
-[testenv:flake8]
-deps =  flake8
-        flake8-import-order
-commands =
-        flake8 src/ tests/
-
-
-#######################################################################################
-[testenv:pylint]
-deps =  {[testenv]deps}
-        -r{toxinidir}/requirements-test.txt
-        pylint==2.10.2
-commands =
-    pylint -E src/ tests/
-
-
-#######################################################################################
-[testenv:safety]
-setenv =
-        LC_ALL=C.UTF-8
-        LANG=C.UTF-8
-deps =  {[testenv]deps}
-        safety
-commands =
-        - safety check --full-report
-
-
-#######################################################################################
-[testenv:yamllint]
-deps =  {[testenv]deps}
-        -r{toxinidir}/requirements-test.txt
-        yamllint
-commands = yamllint .
-
-#######################################################################################
-[testenv:build]
-passenv=HTTP_PROXY HTTPS_PROXY NO_PROXY
-whitelist_externals =
-  charmcraft
-  sh
-commands =
-  charmcraft pack
-  sh -c 'ubuntu_version=20.04; \
-        architectures="amd64-aarch64-arm64"; \
-        charm_name=`cat metadata.yaml | grep -E "^name: " | cut -f 2 -d " "`; \
-        mv $charm_name"_ubuntu-"$ubuntu_version-$architectures.charm $charm_name.charm'
-
-#######################################################################################
-[flake8]
-ignore =
-        W291,
-        W293,
-        W503,
-        E123,
-        E125,
-        E226,
-        E241,
-exclude =
-        .git,
-        __pycache__,
-        .tox,
-max-line-length = 120
-show-source = True
-builtins = _
-max-complexity = 10
-import-order-style = google
diff --git a/installers/charm/osm-keystone/.gitignore b/installers/charm/osm-keystone/.gitignore
deleted file mode 100644 (file)
index 87d0a58..0000000
+++ /dev/null
@@ -1,29 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-venv/
-build/
-*.charm
-.tox/
-.coverage
-coverage.xml
-__pycache__/
-*.py[cod]
-.vscode
\ No newline at end of file
diff --git a/installers/charm/osm-keystone/.jujuignore b/installers/charm/osm-keystone/.jujuignore
deleted file mode 100644 (file)
index 17c7a8b..0000000
+++ /dev/null
@@ -1,23 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-/venv
-*.py[cod]
-*.charm
diff --git a/installers/charm/osm-keystone/CONTRIBUTING.md b/installers/charm/osm-keystone/CONTRIBUTING.md
deleted file mode 100644 (file)
index 3d86cf8..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-# 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.
-#
-
-# Contributing
-
-## Overview
-
-This documents explains the processes and practices recommended for contributing enhancements to
-the Keystone charm.
-
-- If you would like to chat with us about your use-cases or proposed implementation, you can reach
-  us at [OSM public channel](https://opensourcemano.slack.com/archives/CA2TLA48Y)
-- 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 gerrit patch onto
-  the `master` branch.
-
-## 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-keystone
-# Enable DEBUG logging
-juju model-config logging-config="<root>=INFO;unit=DEBUG"
-# Deploy the charm
-juju deploy ./keystone_ubuntu-22.04-amd64.charm \
-  --resource keystone-image=opensourcemano/keystone:testing-daily --series jammy
-```
diff --git a/installers/charm/osm-keystone/LICENSE b/installers/charm/osm-keystone/LICENSE
deleted file mode 100644 (file)
index d645695..0000000
+++ /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/installers/charm/osm-keystone/README.md b/installers/charm/osm-keystone/README.md
deleted file mode 100644 (file)
index 08761b9..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-# 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.
-#
-
-# Keystone Operator
-
-[![code style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black/tree/main)
-
-[![Keystone](https://charmhub.io/osm-keystone/badge.svg)](https://charmhub.io/osm-keystone)
-
-## Description
-
-This charm deploys Keystone in K8s. It is mainly developed to be used as part of the OSM deployment.
-
-## Usage
-
-The Keystone Operator may be deployed using the Juju command line as in
-
-```shell
-$ juju add-model keystone
-$ juju deploy charmed-osm-mariadb-k8s db
-$ juju deploy osm-keystone --trust
-$ juju relate osm-keystone db
-```
-
-## OCI Images
-
-- [keystone](https://hub.docker.com/r/opensourcemano/keystone)
-
-## 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/osm-keystone/actions.yaml b/installers/charm/osm-keystone/actions.yaml
deleted file mode 100644 (file)
index 85ed7e6..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-
-db-sync:
-  description: Execute `keystone-manage db_sync` in the workload container.
diff --git a/installers/charm/osm-keystone/charmcraft.yaml b/installers/charm/osm-keystone/charmcraft.yaml
deleted file mode 100644 (file)
index c8374f3..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-
-type: "charm"
-bases:
-  - build-on:
-      - name: "ubuntu"
-        channel: "22.04"
-    run-on:
-      - name: "ubuntu"
-        channel: "22.04"
-parts:
-  charm:
-    build-packages:
-      - git
diff --git a/installers/charm/osm-keystone/config.yaml b/installers/charm/osm-keystone/config.yaml
deleted file mode 100644 (file)
index 7312bb4..0000000
+++ /dev/null
@@ -1,221 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-
-options:
-  region-id:
-    type: string
-    description: Region ID to be created when starting the service
-    default: RegionOne
-  keystone-db-password:
-    type: string
-    description: Keystone DB Password
-    default: admin
-  admin-username:
-    type: string
-    description: Admin username to be created when starting the service
-    default: admin
-  admin-password:
-    type: string
-    description: Admin password to be created when starting the service
-    default: admin
-  admin-project:
-    type: string
-    description: Admin project to be created when starting the service
-    default: admin
-  service-username:
-    type: string
-    description: Service Username to be created when starting the service
-    default: nbi
-  service-password:
-    type: string
-    description: Service Password to be created when starting the service
-    default: nbi
-  service-project:
-    type: string
-    description: Service Project to be created when starting the service
-    default: service
-  user-domain-name:
-    type: string
-    description: User domain name (Hardcoded in the container start.sh script)
-    default: default
-  project-domain-name:
-    type: string
-    description: |
-      Project domain name (Hardcoded in the container start.sh script)
-    default: default
-  token-expiration:
-    type: int
-    description: Token keys expiration in seconds
-    default: 3600
-  ldap-enabled:
-    type: boolean
-    description: Boolean to enable/disable LDAP authentication
-    default: false
-  ldap-authentication-domain-name:
-    type: string
-    description: Name of the domain which use LDAP authentication
-    default: ""
-  ldap-url:
-    type: string
-    description: URL of the LDAP server
-    default: "ldap://localhost"
-  ldap-bind-user:
-    type: string
-    description: User to bind and search for users
-    default: ""
-  ldap-bind-password:
-    type: string
-    description: Password to bind and search for users
-    default: ""
-  ldap-chase-referrals:
-    type: string
-    description: |
-      Sets keystone’s referral chasing behavior across directory partitions.
-      If left unset, the system’s default behavior will be used.
-    default: ""
-  ldap-page-size:
-    type: int
-    description: |
-      Defines the maximum number of results per page that keystone should
-      request from the LDAP server when listing objects. A value of zero (0)
-      disables paging.
-    default: 0
-  ldap-user-tree-dn:
-    type: string
-    description: |
-      Root of the tree in LDAP server in which Keystone will search for users
-    default: ""
-  ldap-user-objectclass:
-    type: string
-    description: |
-      LDAP object class that Keystone will filter on within user_tree_dn to
-      find user objects. Any objects of other classes will be ignored.
-    default: inetOrgPerson
-  ldap-user-id-attribute:
-    type: string
-    description: |
-      This set of options define the mapping to LDAP attributes for the three
-      key user attributes supported by Keystone. The LDAP attribute chosen for
-      user_id must be something that is immutable for a user and no more than
-      64 characters in length. Notice that Distinguished Name (DN) may be
-      longer than 64 characters and thus is not suitable. An uid, or mail may
-      be appropriate.
-    default: cn
-  ldap-user-name-attribute:
-    type: string
-    description: |
-      This set of options define the mapping to LDAP attributes for the three
-      key user attributes supported by Keystone. The LDAP attribute chosen for
-      user_id must be something that is immutable for a user and no more than
-      64 characters in length. Notice that Distinguished Name (DN) may be
-      longer than 64 characters and thus is not suitable. An uid, or mail may
-      be appropriate.
-    default: sn
-  ldap-user-pass-attribute:
-    type: string
-    description: |
-      This set of options define the mapping to LDAP attributes for the three
-      key user attributes supported by Keystone. The LDAP attribute chosen for
-      user_id must be something that is immutable for a user and no more than
-      64 characters in length. Notice that Distinguished Name (DN) may be
-      longer than 64 characters and thus is not suitable. An uid, or mail may
-      be appropriate.
-    default: userPassword
-  ldap-user-filter:
-    type: string
-    description: |
-      This filter option allow additional filter (over and above
-      user_objectclass) to be included into the search of user. One common use
-      of this is to provide more efficient searching, where the recommended
-      search for user objects is (&(objectCategory=person)(objectClass=user)).
-      By specifying user_objectclass as user and user_filter as
-      objectCategory=person in the Keystone configuration file, this can be
-      achieved.
-    default: ""
-  ldap-user-enabled-attribute:
-    type: string
-    description: |
-      In Keystone, a user entity can be either enabled or disabled. Setting
-      the above option will give a mapping to an equivalent attribute in LDAP,
-      allowing your LDAP management tools to disable a user.
-    default: enabled
-  ldap-user-enabled-mask:
-    type: int
-    description: |
-      Some LDAP schemas, rather than having a dedicated attribute for user
-      enablement, use a bit within a general control attribute (such as
-      userAccountControl) to indicate this. Setting user_enabled_mask will
-      cause Keystone to look at only the status of this bit in the attribute
-      specified by user_enabled_attribute, with the bit set indicating the
-      user is enabled.
-    default: 0
-  ldap-user-enabled-default:
-    type: string
-    description: |
-      Most LDAP servers use a boolean or bit in a control field to indicate
-      enablement. However, some schemas might use an integer value in an
-      attribute. In this situation, set user_enabled_default to the integer
-      value that represents a user being enabled.
-    default: "true"
-  ldap-user-enabled-invert:
-    type: boolean
-    description: |
-      Some LDAP schemas have an “account locked” attribute, which is the
-      equivalent to account being “disabled.” In order to map this to the
-      Keystone enabled attribute, you can utilize the user_enabled_invert
-      setting in conjunction with user_enabled_attribute to map the lock
-      status to disabled in Keystone.
-    default: false
-  ldap-group-objectclass:
-    type: string
-    description: The LDAP object class to use for groups.
-    default: groupOfNames
-  ldap-group-tree-dn:
-    type: string
-    description: The search base to use for groups.
-    default: ""
-  ldap-use-starttls:
-    type: boolean
-    description: |
-      Enable Transport Layer Security (TLS) for providing a secure connection
-      from Keystone to LDAP (StartTLS, not LDAPS).
-    default: false
-  ldap-tls-cacert-base64:
-    type: string
-    description: |
-      CA certificate in Base64 format (if you have the PEM file, text inside
-      "-----BEGIN CERTIFICATE-----"/"-----END CERTIFICATE-----" tags).
-    default: ""
-  ldap-tls-req-cert:
-    type: string
-    description: |
-      Defines how the certificates are checked for validity in the client
-      (i.e., Keystone end) of the secure connection (this doesn’t affect what
-      level of checking the server is doing on the certificates it receives
-      from Keystone). Possible values are "demand", "never", and "allow". The
-      default of demand means the client always checks the certificate and
-      will drop the connection if it is not provided or invalid. never is the
-      opposite—it never checks it, nor requires it to be provided. allow means
-      that if it is not provided then the connection is allowed to continue,
-      but if it is provided it will be checked—and if invalid, the connection
-      will be dropped.
-    default: demand
-  mysql-uri:
-    type: string
-    description: |
-      Mysql URI with the following format:
-        mysql://<user>:<password>@<mysql_host>:<mysql_port>/<database>
diff --git a/installers/charm/osm-keystone/lib/charms/observability_libs/v0/kubernetes_service_patch.py b/installers/charm/osm-keystone/lib/charms/observability_libs/v0/kubernetes_service_patch.py
deleted file mode 100644 (file)
index 39b364b..0000000
+++ /dev/null
@@ -1,253 +0,0 @@
-# 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.
-#
-
-"""# KubernetesServicePatch Library.
-
-This library is designed to enable developers to more simply patch the Kubernetes Service created
-by Juju during the deployment of a sidecar charm. When sidecar charms are deployed, Juju creates a
-service named after the application in the namespace (named after the Juju model). This service by
-default contains a "placeholder" port, which is 65536/TCP.
-
-When modifying the default set of resources managed by Juju, one must consider the lifecycle of the
-charm. In this case, any modifications to the default service (created during deployment), will
-be overwritten during a charm upgrade.
-
-When intialised, this library binds a handler to the parent charm's `install` and `upgrade_charm`
-events which applies the patch to the cluster. This should ensure that the service ports are
-correct throughout the charm's life.
-
-The constructor simply takes a reference to the parent charm, and a list of tuples that each define
-a port for the service, where each tuple contains:
-
-- a name for the port
-- port for the service to listen on
-- optionally: a targetPort for the service (the port in the container!)
-- optionally: a nodePort for the service (for NodePort or LoadBalancer services only!)
-- optionally: a name of the service (in case service name needs to be patched as well)
-
-## Getting Started
-
-To get started using the library, you just need to fetch the library using `charmcraft`. **Note
-that you also need to add `lightkube` and `lightkube-models` to your charm's `requirements.txt`.**
-
-```shell
-cd some-charm
-charmcraft fetch-lib charms.observability_libs.v0.kubernetes_service_patch
-echo <<-EOF >> requirements.txt
-lightkube
-lightkube-models
-EOF
-```
-
-Then, to initialise the library:
-
-For ClusterIP services:
-```python
-# ...
-from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    self.service_patcher = KubernetesServicePatch(self, [(f"{self.app.name}", 8080)])
-    # ...
-```
-
-For LoadBalancer/NodePort services:
-```python
-# ...
-from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    self.service_patcher = KubernetesServicePatch(
-        self, [(f"{self.app.name}", 443, 443, 30666)], "LoadBalancer"
-    )
-    # ...
-```
-
-Additionally, you may wish to use mocks in your charm's unit testing to ensure that the library
-does not try to make any API calls, or open any files during testing that are unlikely to be
-present, and could break your tests. The easiest way to do this is during your test `setUp`:
-
-```python
-# ...
-
-@patch("charm.KubernetesServicePatch", lambda x, y: None)
-def setUp(self, *unused):
-    self.harness = Harness(SomeCharm)
-    # ...
-```
-"""
-
-import logging
-from types import MethodType
-from typing import Literal, Sequence, Tuple, Union
-
-from lightkube import ApiError, Client
-from lightkube.models.core_v1 import ServicePort, ServiceSpec
-from lightkube.models.meta_v1 import ObjectMeta
-from lightkube.resources.core_v1 import Service
-from lightkube.types import PatchType
-from ops.charm import CharmBase
-from ops.framework import Object
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0042f86d0a874435adef581806cddbbb"
-
-# 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 = 5
-
-PortDefinition = Union[Tuple[str, int], Tuple[str, int, int], Tuple[str, int, int, int]]
-ServiceType = Literal["ClusterIP", "LoadBalancer"]
-
-
-class KubernetesServicePatch(Object):
-    """A utility for patching the Kubernetes service set up by Juju."""
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        ports: Sequence[PortDefinition],
-        service_name: str = None,
-        service_type: ServiceType = "ClusterIP",
-    ):
-        """Constructor for KubernetesServicePatch.
-
-        Args:
-            charm: the charm that is instantiating the library.
-            ports: a list of tuples (name, port, targetPort, nodePort) for every service port.
-            service_name: allows setting custom name to the patched service. If none given,
-                application name will be used.
-            service_type: desired type of K8s service. Default value is in line with ServiceSpec's
-                default value.
-        """
-        super().__init__(charm, "kubernetes-service-patch")
-        self.charm = charm
-        self.service_name = service_name if service_name else self._app
-        self.service = self._service_object(ports, service_name, service_type)
-
-        # Make mypy type checking happy that self._patch is a method
-        assert isinstance(self._patch, MethodType)
-        # Ensure this patch is applied during the 'install' and 'upgrade-charm' events
-        self.framework.observe(charm.on.install, self._patch)
-        self.framework.observe(charm.on.upgrade_charm, self._patch)
-
-    def _service_object(
-        self,
-        ports: Sequence[PortDefinition],
-        service_name: str = None,
-        service_type: ServiceType = "ClusterIP",
-    ) -> Service:
-        """Creates a valid Service representation for Alertmanager.
-
-        Args:
-            ports: a list of tuples of the form (name, port) or (name, port, targetPort)
-                or (name, port, targetPort, nodePort) for every service port. If the 'targetPort'
-                is omitted, it is assumed to be equal to 'port', with the exception of NodePort
-                and LoadBalancer services, where all port numbers have to be specified.
-            service_name: allows setting custom name to the patched service. If none given,
-                application name will be used.
-            service_type: desired type of K8s service. Default value is in line with ServiceSpec's
-                default value.
-
-        Returns:
-            Service: A valid representation of a Kubernetes Service with the correct ports.
-        """
-        if not service_name:
-            service_name = self._app
-        return Service(
-            apiVersion="v1",
-            kind="Service",
-            metadata=ObjectMeta(
-                namespace=self._namespace,
-                name=service_name,
-                labels={"app.kubernetes.io/name": service_name},
-            ),
-            spec=ServiceSpec(
-                selector={"app.kubernetes.io/name": service_name},
-                ports=[
-                    ServicePort(
-                        name=p[0],
-                        port=p[1],
-                        targetPort=p[2] if len(p) > 2 else p[1],  # type: ignore[misc]
-                        nodePort=p[3] if len(p) > 3 else None,  # type: ignore[arg-type, misc]
-                    )
-                    for p in ports
-                ],
-                type=service_type,
-            ),
-        )
-
-    def _patch(self, _) -> None:
-        """Patch the Kubernetes service created by Juju to map the correct port.
-
-        Raises:
-            PatchFailed: if patching fails due to lack of permissions, or otherwise.
-        """
-        if not self.charm.unit.is_leader():
-            return
-
-        client = Client()
-        try:
-            client.patch(Service, self._app, self.service, patch_type=PatchType.MERGE)
-        except ApiError as e:
-            if e.status.code == 403:
-                logger.error("Kubernetes service patch failed: `juju trust` this application.")
-            else:
-                logger.error("Kubernetes service patch failed: %s", str(e))
-        else:
-            logger.info("Kubernetes service '%s' patched successfully", self._app)
-
-    def is_patched(self) -> bool:
-        """Reports if the service patch has been applied.
-
-        Returns:
-            bool: A boolean indicating if the service patch has been applied.
-        """
-        client = Client()
-        # Get the relevant service from the cluster
-        service = client.get(Service, name=self.service_name, namespace=self._namespace)
-        # Construct a list of expected ports, should the patch be applied
-        expected_ports = [(p.port, p.targetPort) for p in self.service.spec.ports]
-        # Construct a list in the same manner, using the fetched service
-        fetched_ports = [(p.port, p.targetPort) for p in service.spec.ports]  # type: ignore[attr-defined]  # noqa: E501
-        return expected_ports == fetched_ports
-
-    @property
-    def _app(self) -> str:
-        """Name of the current Juju application.
-
-        Returns:
-            str: A string containing the name of the current Juju application.
-        """
-        return self.charm.app.name
-
-    @property
-    def _namespace(self) -> str:
-        """The Kubernetes namespace we're running in.
-
-        Returns:
-            str: A string containing the name of the current Kubernetes namespace.
-        """
-        with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") as f:
-            return f.read().strip()
diff --git a/installers/charm/osm-keystone/metadata.yaml b/installers/charm/osm-keystone/metadata.yaml
deleted file mode 100644 (file)
index 61a412b..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-# 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.
-#
-
-name: osm-keystone
-display-name: Keystone
-description: |
-  Keystone operator used for Charmed OSM
-
-summary: |
-  Keystone operator used for Charmed OSM
-
-containers:
-  keystone:
-    resource: keystone-image
-
-resources:
-  keystone-image:
-    type: oci-image
-    description: OCI image for Keystone
-    upstream-source: opensourcemano/keystone:testing-daily
-
-requires:
-  db:
-    interface: mysql
-    limit: 1
-
-peers:
-  cluster:
-    interface: cluster
-
-provides:
-  keystone:
-    interface: keystone
diff --git a/installers/charm/osm-keystone/pyproject.toml b/installers/charm/osm-keystone/pyproject.toml
deleted file mode 100644 (file)
index af62f24..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-# 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.
-#
-
-# 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", "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/osm-keystone/requirements.txt b/installers/charm/osm-keystone/requirements.txt
deleted file mode 100644 (file)
index 4284431..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-ops < 2.2
-git+https://github.com/charmed-osm/config-validator/
-lightkube
-lightkube-models
\ No newline at end of file
diff --git a/installers/charm/osm-keystone/src/charm.py b/installers/charm/osm-keystone/src/charm.py
deleted file mode 100755 (executable)
index c368ade..0000000
+++ /dev/null
@@ -1,443 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Actions tab on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
-
-"""Keystone charm module."""
-
-import logging
-from datetime import datetime
-
-from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
-from config_validator import ValidationError
-from ops import pebble
-from ops.charm import ActionEvent, CharmBase, ConfigChangedEvent, UpdateStatusEvent
-from ops.main import main
-from ops.model import ActiveStatus, BlockedStatus, Container, MaintenanceStatus
-
-import cluster
-from config import ConfigModel, MysqlConnectionData, get_environment, validate_config
-from interfaces import KeystoneServer, MysqlClient
-
-logger = logging.getLogger(__name__)
-
-
-# We expect the keystone container to use the default port
-PORT = 5000
-
-KEY_SETUP_FILE = "/etc/keystone/key-setup"
-CREDENTIAL_KEY_REPOSITORY = "/etc/keystone/credential-keys/"
-FERNET_KEY_REPOSITORY = "/etc/keystone/fernet-keys/"
-KEYSTONE_USER = "keystone"
-KEYSTONE_GROUP = "keystone"
-FERNET_MAX_ACTIVE_KEYS = 3
-KEYSTONE_FOLDER = "/etc/keystone/"
-
-
-class CharmError(Exception):
-    """Charm error exception."""
-
-
-class KeystoneCharm(CharmBase):
-    """Keystone Charm operator."""
-
-    on = cluster.ClusterEvents()
-
-    def __init__(self, *args) -> None:
-        super().__init__(*args)
-        event_observe_mapping = {
-            self.on.keystone_pebble_ready: self._on_config_changed,
-            self.on.config_changed: self._on_config_changed,
-            self.on.update_status: self._on_update_status,
-            self.on.cluster_keys_changed: self._on_cluster_keys_changed,
-            self.on["keystone"].relation_joined: self._publish_keystone_info,
-            self.on["db"].relation_changed: self._on_config_changed,
-            self.on["db"].relation_broken: self._on_config_changed,
-            self.on["db-sync"].action: self._on_db_sync_action,
-        }
-        for event, observer in event_observe_mapping.items():
-            self.framework.observe(event, observer)
-        self.cluster = cluster.Cluster(self)
-        self.mysql_client = MysqlClient(self, relation_name="db")
-        self.keystone = KeystoneServer(self, relation_name="keystone")
-        self.service_patch = KubernetesServicePatch(self, [(f"{self.app.name}", PORT)])
-
-    @property
-    def container(self) -> Container:
-        """Property to get keystone container."""
-        return self.unit.get_container("keystone")
-
-    def _on_db_sync_action(self, event: ActionEvent):
-        process = self.container.exec(["keystone-manage", "db_sync"])
-        try:
-            process.wait()
-            event.set_results({"output": "db-sync was successfully executed."})
-        except pebble.ExecError as e:
-            error_message = f"db-sync action failed with code {e.exit_code} and stderr {e.stderr}."
-            logger.error(error_message)
-            event.fail(error_message)
-
-    def _publish_keystone_info(self, _):
-        """Handler for keystone-relation-joined."""
-        if self.unit.is_leader():
-            config = ConfigModel(**dict(self.config))
-            self.keystone.publish_info(
-                host=f"http://{self.app.name}:{PORT}/v3",
-                port=PORT,
-                user_domain_name=config.user_domain_name,
-                project_domain_name=config.project_domain_name,
-                username=config.service_username,
-                password=config.service_password,
-                service=config.service_project,
-                keystone_db_password=config.keystone_db_password,
-                region_id=config.region_id,
-                admin_username=config.admin_username,
-                admin_password=config.admin_password,
-                admin_project_name=config.admin_project,
-            )
-
-    def _on_config_changed(self, _: ConfigChangedEvent) -> None:
-        """Handler for config-changed event."""
-        if self.container.can_connect():
-            try:
-                self._handle_fernet_key_rotation()
-                self._safe_restart()
-                self.unit.status = ActiveStatus()
-            except CharmError as e:
-                self.unit.status = BlockedStatus(str(e))
-            except ValidationError as e:
-                self.unit.status = BlockedStatus(str(e))
-        else:
-            logger.info("pebble socket not available, deferring config-changed")
-            self.unit.status = MaintenanceStatus("waiting for pebble to start")
-
-    def _on_update_status(self, event: UpdateStatusEvent) -> None:
-        """Handler for update-status event."""
-        if self.container.can_connect():
-            self._handle_fernet_key_rotation()
-        else:
-            logger.info("pebble socket not available, deferring config-changed")
-            event.defer()
-            self.unit.status = MaintenanceStatus("waiting for pebble to start")
-
-    def _on_cluster_keys_changed(self, _) -> None:
-        """Handler for ClusterKeysChanged event."""
-        self._handle_fernet_key_rotation()
-
-    def _handle_fernet_key_rotation(self) -> None:
-        """Handles fernet key rotation.
-
-        First, the function writes the existing keys in the relation to disk.
-        Then, if the unit is the leader, checks if the keys should be rotated
-        or not.
-        """
-        self._key_write()
-        if self.unit.is_leader():
-            if not self.cluster.get_keys():
-                self._key_setup()
-            self._fernet_keys_rotate_and_sync()
-
-    def _key_write(self) -> None:
-        """Write keys to container from the relation data."""
-        if self.unit.is_leader():
-            return
-        keys = self.cluster.get_keys()
-        if not keys:
-            logger.debug('"key_repository" not in relation data yet...')
-            return
-
-        for key_repository in [FERNET_KEY_REPOSITORY, CREDENTIAL_KEY_REPOSITORY]:
-            self._create_keys_folders()
-            for key_number, key in keys[key_repository].items():
-                logger.debug(f"writing key {key_number} in {key_repository}")
-                file_path = f"{key_repository}{key_number}"
-                if self._file_changed(file_path, key):
-                    self.container.push(
-                        file_path,
-                        key,
-                        user=KEYSTONE_USER,
-                        group=KEYSTONE_GROUP,
-                        permissions=0o600,
-                    )
-        self.container.push(KEY_SETUP_FILE, "")
-
-    def _file_changed(self, file_path: str, content: str) -> bool:
-        """Check if file in container has changed its value.
-
-        This function checks if the file exists in the container. If it does,
-        then it checks if the content of that file is equal to the content passed to
-        this function. If the content is equal, the function returns False, otherwise True.
-
-        Args:
-            file_path (str): File path in the container.
-            content (str): Content of the file.
-
-        Returns:
-            bool: True if the content of the file has changed, or the file doesn't exist in
-                  the container. False if the content passed to this function is the same as
-                  in the container.
-        """
-        if self._file_exists(file_path):
-            old_content = self.container.pull(file_path).read()
-            if old_content == content:
-                return False
-        return True
-
-    def _create_keys_folders(self) -> None:
-        """Create folders for Key repositories."""
-        fernet_key_repository_found = False
-        credential_key_repository_found = False
-        for file in self.container.list_files(KEYSTONE_FOLDER):
-            if file.type == pebble.FileType.DIRECTORY:
-                if file.path == CREDENTIAL_KEY_REPOSITORY:
-                    credential_key_repository_found = True
-                if file.path == FERNET_KEY_REPOSITORY:
-                    fernet_key_repository_found = True
-        if not fernet_key_repository_found:
-            self.container.make_dir(
-                FERNET_KEY_REPOSITORY,
-                user="keystone",
-                group="keystone",
-                permissions=0o700,
-                make_parents=True,
-            )
-        if not credential_key_repository_found:
-            self.container.make_dir(
-                CREDENTIAL_KEY_REPOSITORY,
-                user=KEYSTONE_USER,
-                group=KEYSTONE_GROUP,
-                permissions=0o700,
-                make_parents=True,
-            )
-
-    def _fernet_keys_rotate_and_sync(self) -> None:
-        """Rotate and sync the keys if the unit is the leader and the primary key has expired.
-
-        The modification time of the staging key (key with index '0') is used,
-        along with the config setting "token-expiration" to determine whether to
-        rotate the keys.
-
-        The rotation time = token-expiration / (max-active-keys - 2)
-        where max-active-keys has a minimum of 3.
-        """
-        if not self.unit.is_leader():
-            return
-        try:
-            fernet_key_file = self.container.list_files(f"{FERNET_KEY_REPOSITORY}0")[0]
-            last_rotation = fernet_key_file.last_modified.timestamp()
-        except pebble.APIError:
-            logger.warning(
-                "Fernet key rotation requested but key repository not " "initialized yet"
-            )
-            return
-
-        config = ConfigModel(**self.config)
-        rotation_time = config.token_expiration // (FERNET_MAX_ACTIVE_KEYS - 2)
-
-        now = datetime.now().timestamp()
-        if last_rotation + rotation_time > now:
-            # No rotation to do as not reached rotation time
-            logger.debug("No rotation needed")
-            self._key_leader_set()
-            return
-        # now rotate the keys and sync them
-        self._fernet_rotate()
-        self._key_leader_set()
-
-        logger.info("Rotated and started sync of fernet keys")
-
-    def _key_leader_set(self) -> None:
-        """Read current key sets and update peer relation data.
-
-        The keys are read from the `FERNET_KEY_REPOSITORY` and `CREDENTIAL_KEY_REPOSITORY`
-        directories. Note that this function will fail if it is called on the unit that is
-        not the leader.
-        """
-        disk_keys = {}
-        for key_repository in [FERNET_KEY_REPOSITORY, CREDENTIAL_KEY_REPOSITORY]:
-            disk_keys[key_repository] = {}
-            for file in self.container.list_files(key_repository):
-                key_content = self.container.pull(f"{key_repository}{file.name}").read()
-                disk_keys[key_repository][file.name] = key_content
-        self.cluster.save_keys(disk_keys)
-
-    def _fernet_rotate(self) -> None:
-        """Rotate Fernet keys.
-
-        To rotate the Fernet tokens, and create a new staging key, it calls (as the
-        "keystone" user):
-
-            keystone-manage fernet_rotate
-
-        Note that we do not rotate the Credential encryption keys.
-
-        Note that this does NOT synchronise the keys between the units.  This is
-        performed in `self._key_leader_set`.
-        """
-        logger.debug("Rotating Fernet tokens")
-        try:
-            exec_command = [
-                "keystone-manage",
-                "fernet_rotate",
-                "--keystone-user",
-                KEYSTONE_USER,
-                "--keystone-group",
-                KEYSTONE_GROUP,
-            ]
-            logger.debug(f'Executing command: {" ".join(exec_command)}')
-            self.container.exec(exec_command).wait()
-            logger.info("Fernet keys successfully rotated.")
-        except pebble.ExecError as e:
-            logger.error(f"Fernet Key rotation failed: {e}")
-            logger.error("Exited with code %d. Stderr:", e.exit_code)
-            for line in e.stderr.splitlines():
-                logger.error("    %s", line)
-
-    def _key_setup(self) -> None:
-        """Initialize Fernet and Credential encryption key repositories.
-
-        To setup the key repositories:
-
-            keystone-manage fernet_setup
-            keystone-manage credential_setup
-
-        In addition we migrate any credentials currently stored in database using
-        the null key to be encrypted by the new credential key:
-
-            keystone-manage credential_migrate
-
-        Note that we only want to do this once, so we touch an empty file
-        (KEY_SETUP_FILE) to indicate that it has been done.
-        """
-        if self._file_exists(KEY_SETUP_FILE) or not self.unit.is_leader():
-            return
-
-        logger.debug("Setting up key repositories for Fernet tokens and Credential encryption.")
-        try:
-            for command in ["fernet_setup", "credential_setup"]:
-                exec_command = [
-                    "keystone-manage",
-                    command,
-                    "--keystone-user",
-                    KEYSTONE_USER,
-                    "--keystone-group",
-                    KEYSTONE_GROUP,
-                ]
-                logger.debug(f'Executing command: {" ".join(exec_command)}')
-                self.container.exec(exec_command).wait()
-            self.container.push(KEY_SETUP_FILE, "")
-            logger.info("Key repositories initialized successfully.")
-        except pebble.ExecError as e:
-            logger.error("Failed initializing key repositories.")
-            logger.error("Exited with code %d. Stderr:", e.exit_code)
-            for line in e.stderr.splitlines():
-                logger.error("    %s", line)
-
-    def _file_exists(self, path: str) -> bool:
-        """Check if a file exists in the container.
-
-        Args:
-            path (str): Path of the file to be checked.
-
-        Returns:
-            bool: True if the file exists, else False.
-        """
-        file_exists = None
-        try:
-            _ = self.container.pull(path)
-            file_exists = True
-        except pebble.PathError:
-            file_exists = False
-        exist_str = "exists" if file_exists else 'doesn"t exist'
-        logger.debug(f"File {path} {exist_str}.")
-        return file_exists
-
-    def _safe_restart(self) -> None:
-        """Safely restart the keystone service.
-
-        This function (re)starts the keystone service after doing some safety checks,
-        like validating the charm configuration, checking the mysql relation is ready.
-        """
-        validate_config(self.config)
-        self._check_mysql_data()
-        # Workaround: OS_AUTH_URL is not ready when the entrypoint restarts apache2.
-        # The function `self._patch_entrypoint` fixes that.
-        self._patch_entrypoint()
-        self._replan()
-
-    def _patch_entrypoint(self) -> None:
-        """Patches the entrypoint of the Keystone service.
-
-        The entrypoint that restarts apache2, expects immediate communication to OS_AUTH_URL.
-        This does not happen instantly. This function patches the entrypoint to wait until a
-        curl to OS_AUTH_URL succeeds.
-        """
-        installer_script = self.container.pull("/app/start.sh").read()
-        wait_until_ready_command = "until $(curl --output /dev/null --silent --head --fail $OS_AUTH_URL); do echo '...'; sleep 5; done"
-        self.container.push(
-            "/app/start-patched.sh",
-            installer_script.replace(
-                "source setup_env", f"source setup_env && {wait_until_ready_command}"
-            ),
-            permissions=0o755,
-        )
-
-    def _check_mysql_data(self) -> None:
-        """Check if the mysql relation is ready.
-
-        Raises:
-            CharmError: Error raised if the mysql relation is not ready.
-        """
-        if self.mysql_client.is_missing_data_in_unit() and not self.config.get("mysql-uri"):
-            raise CharmError("mysql relation is missing")
-
-    def _replan(self) -> None:
-        """Replan keystone service.
-
-        This function starts the keystone service if it is not running.
-        If the service started already, this function will restart the
-        service if there are any changes to the layer.
-        """
-        mysql_data = MysqlConnectionData(
-            self.config.get("mysql-uri")
-            or f"mysql://root:{self.mysql_client.root_password}@{self.mysql_client.host}:{self.mysql_client.port}/"
-        )
-        layer = {
-            "summary": "keystone layer",
-            "description": "pebble config layer for keystone",
-            "services": {
-                "keystone": {
-                    "override": "replace",
-                    "summary": "keystone service",
-                    "command": "/app/start-patched.sh",
-                    "startup": "enabled",
-                    "environment": get_environment(self.app.name, self.config, mysql_data),
-                }
-            },
-        }
-        self.container.add_layer("keystone", layer, combine=True)
-        self.container.replan()
-
-
-if __name__ == "__main__":  # pragma: no cover
-    main(KeystoneCharm)
diff --git a/installers/charm/osm-keystone/src/cluster.py b/installers/charm/osm-keystone/src/cluster.py
deleted file mode 100644 (file)
index f38adec..0000000
+++ /dev/null
@@ -1,135 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Actions tab on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
-
-"""Keystone cluster library.
-
-This library allows the integration with Apache Guacd charm. Is is published as part of the
-[davigar15-apache-guacd]((https://charmhub.io/davigar15-apache-guacd) charm.
-
-The charm that requires guacd should include the following content in its metadata.yaml:
-
-```yaml
-# ...
-peers:
-    cluster:
-        interface: cluster
-# ...
-```
-
-A typical example of including this library might be:
-
-```python
-# ...
-from ops.framework import StoredState
-from charms.keystone.v0 import cluster
-
-class SomeApplication(CharmBase):
-  on = cluster.ClusterEvents()
-
-  def __init__(self, *args):
-    # ...
-    self.cluster = cluster.Cluster(self)
-    self.framework.observe(self.on.cluster_keys_changed, self._cluster_keys_changed)
-    # ...
-
-  def _cluster_keys_changed(self, _):
-    fernet_keys = self.cluster.fernet_keys
-    credential_keys = self.cluster.credential_keys
-    # ...
-```
-"""
-
-
-import json
-import logging
-from typing import Any, Dict, List
-
-from ops.charm import CharmEvents
-from ops.framework import EventBase, EventSource, Object
-from ops.model import Relation
-
-# Number of keys need might need to be adjusted in the future
-NUMBER_FERNET_KEYS = 2
-NUMBER_CREDENTIAL_KEYS = 2
-
-logger = logging.getLogger(__name__)
-
-
-class ClusterKeysChangedEvent(EventBase):
-    """Event to announce a change in the Guacd service."""
-
-
-class ClusterEvents(CharmEvents):
-    """Cluster Events."""
-
-    cluster_keys_changed = EventSource(ClusterKeysChangedEvent)
-
-
-class Cluster(Object):
-    """Peer relation."""
-
-    def __init__(self, charm):
-        super().__init__(charm, "cluster")
-        self.charm = charm
-
-    @property
-    def fernet_keys(self) -> List[str]:
-        """Fernet keys."""
-        relation: Relation = self.model.get_relation("cluster")
-        application_data = relation.data[self.model.app]
-        return json.loads(application_data.get("keys-fernet", "[]"))
-
-    @property
-    def credential_keys(self) -> List[str]:
-        """Credential keys."""
-        relation: Relation = self.model.get_relation("cluster")
-        application_data = relation.data[self.model.app]
-        return json.loads(application_data.get("keys-credential", "[]"))
-
-    def save_keys(self, keys: Dict[str, Any]) -> None:
-        """Generate fernet and credential keys.
-
-        This method will generate new keys and fire the cluster_keys_changed event.
-        """
-        logger.debug("Saving keys...")
-        relation: Relation = self.model.get_relation("cluster")
-        data = relation.data[self.model.app]
-        current_keys_str = data.get("key_repository", "{}")
-        current_keys = json.loads(current_keys_str)
-        if current_keys != keys:
-            data["key_repository"] = json.dumps(keys)
-            self.charm.on.cluster_keys_changed.emit()
-        logger.info("Keys saved!")
-
-    def get_keys(self) -> Dict[str, Any]:
-        """Get keys from the relation.
-
-        Returns:
-            Dict[str, Any]: Dictionary with the keys.
-        """
-        relation: Relation = self.model.get_relation("cluster")
-        data = relation.data[self.model.app]
-        current_keys_str = data.get("key_repository", "{}")
-        current_keys = json.loads(current_keys_str)
-        return current_keys
diff --git a/installers/charm/osm-keystone/src/config.py b/installers/charm/osm-keystone/src/config.py
deleted file mode 100644 (file)
index 803d564..0000000
+++ /dev/null
@@ -1,184 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Actions tab on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
-
-"""Module that takes take of the charm configuration."""
-
-import re
-from typing import Any, Dict, Optional
-
-from config_validator import ConfigValidator, ValidationError
-from ops.model import ConfigData
-
-
-class MysqlConnectionData:
-    """Mysql Connection Data class."""
-
-    _compiled_regex = re.compile(
-        r"^mysql\:\/\/{}@{}\/{}?$".format(
-            r"(?P<username>[_\w]+):(?P<password>[\w\W]+)",
-            r"(?P<host>[\-\.\w]+):(?P<port>\d+)",
-            r"(?P<database>[_\w]+)",
-        )
-    )
-
-    def __init__(self, mysql_uri: str):
-        match = self._compiled_regex.search(mysql_uri)
-        if not match:
-            raise ValidationError("mysql_uri is not properly formed")
-        mysql_data = match.groupdict()
-        self.host = mysql_data.get("host")
-        self.port = int(mysql_data.get("port"))
-        self.username = mysql_data.get("username")
-        self.password = mysql_data.get("password")
-        self.database = mysql_data.get("database")
-        self.uri = mysql_uri
-
-
-def validate_config(config: ConfigData):
-    """Validate charm configuration.
-
-    Args:
-        config (ConfigData): Charm configuration.
-
-    Raises:
-        config_validator.ValidationError if the validation failed.
-    """
-    kwargs: Dict[str, Any] = config
-    ConfigModel(**kwargs)
-    ConfigLdapModel(**kwargs)
-
-
-def get_environment(
-    service_name: str, config: ConfigData, mysql_data: MysqlConnectionData
-) -> Dict[str, Any]:
-    """Get environment variables.
-
-    Args:
-        service_name (str): Cluster IP service name.
-        config (ConfigData): Charm configuration.
-
-    Returns:
-        Dict[str, Any]: Dictionary with the environment variables for Keystone service.
-    """
-    kwargs: Dict[str, Any] = config
-    config = ConfigModel(**kwargs)
-    config_ldap = ConfigLdapModel(**kwargs)
-    environment = {
-        "DB_HOST": mysql_data.host,
-        "DB_PORT": mysql_data.port,
-        "ROOT_DB_USER": mysql_data.username,
-        "ROOT_DB_PASSWORD": mysql_data.password,
-        "REGION_ID": config.region_id,
-        "KEYSTONE_HOST": service_name,
-        "KEYSTONE_DB_PASSWORD": config.keystone_db_password,
-        "ADMIN_USERNAME": config.admin_username,
-        "ADMIN_PASSWORD": config.admin_password,
-        "ADMIN_PROJECT": config.admin_project,
-        "SERVICE_USERNAME": config.service_username,
-        "SERVICE_PASSWORD": config.service_password,
-        "SERVICE_PROJECT": config.service_project,
-    }
-    if config_ldap.ldap_enabled:
-        environment.update(
-            {
-                "LDAP_AUTHENTICATION_DOMAIN_NAME": config_ldap.ldap_authentication_domain_name,
-                "LDAP_URL": config_ldap.ldap_url,
-                "LDAP_PAGE_SIZE": str(config_ldap.ldap_page_size),
-                "LDAP_USER_OBJECTCLASS": config_ldap.ldap_user_objectclass,
-                "LDAP_USER_ID_ATTRIBUTE": config_ldap.ldap_user_id_attribute,
-                "LDAP_USER_NAME_ATTRIBUTE": config_ldap.ldap_user_name_attribute,
-                "LDAP_USER_PASS_ATTRIBUTE": config_ldap.ldap_user_pass_attribute,
-                "LDAP_USER_ENABLED_MASK": str(config_ldap.ldap_user_enabled_mask),
-                "LDAP_USER_ENABLED_DEFAULT": config_ldap.ldap_user_enabled_default,
-                "LDAP_USER_ENABLED_INVERT": str(config_ldap.ldap_user_enabled_invert),
-                "LDAP_GROUP_OBJECTCLASS": config_ldap.ldap_group_objectclass,
-            }
-        )
-        if config_ldap.ldap_use_starttls:
-            environment.update(
-                {
-                    "LDAP_USE_STARTTLS": str(config_ldap.ldap_use_starttls),
-                    "LDAP_TLS_CACERT_BASE64": config_ldap.ldap_tls_cacert_base64,
-                    "LDAP_TLS_REQ_CERT": config_ldap.ldap_tls_req_cert,
-                }
-            )
-        optional_ldap_configs = {
-            "LDAP_BIND_USER": config_ldap.ldap_bind_user,
-            "LDAP_BIND_PASSWORD": config_ldap.ldap_bind_password,
-            "LDAP_USER_TREE_DN": config_ldap.ldap_user_tree_dn,
-            "LDAP_USER_FILTER": config_ldap.ldap_user_filter,
-            "LDAP_USER_ENABLED_ATTRIBUTE": config_ldap.ldap_user_enabled_attribute,
-            "LDAP_CHASE_REFERRALS": config_ldap.ldap_chase_referrals,
-            "LDAP_GROUP_TREE_DN": config_ldap.ldap_group_tree_dn,
-            "LDAP_TLS_CACERT_BASE64": config_ldap.ldap_tls_cacert_base64,
-        }
-        for env, value in optional_ldap_configs.items():
-            if value:
-                environment[env] = value
-    return environment
-
-
-class ConfigModel(ConfigValidator):
-    """Keystone Configuration."""
-
-    region_id: str
-    keystone_db_password: str
-    admin_username: str
-    admin_password: str
-    admin_project: str
-    service_username: str
-    service_password: str
-    service_project: str
-    user_domain_name: str
-    project_domain_name: str
-    token_expiration: int
-    mysql_uri: Optional[str]
-
-
-class ConfigLdapModel(ConfigValidator):
-    """LDAP Configuration."""
-
-    ldap_enabled: bool
-    ldap_authentication_domain_name: Optional[str]
-    ldap_url: Optional[str]
-    ldap_bind_user: Optional[str]
-    ldap_bind_password: Optional[str]
-    ldap_chase_referrals: Optional[str]
-    ldap_page_size: Optional[int]
-    ldap_user_tree_dn: Optional[str]
-    ldap_user_objectclass: Optional[str]
-    ldap_user_id_attribute: Optional[str]
-    ldap_user_name_attribute: Optional[str]
-    ldap_user_pass_attribute: Optional[str]
-    ldap_user_filter: Optional[str]
-    ldap_user_enabled_attribute: Optional[str]
-    ldap_user_enabled_mask: Optional[int]
-    ldap_user_enabled_default: Optional[str]
-    ldap_user_enabled_invert: Optional[bool]
-    ldap_group_objectclass: Optional[str]
-    ldap_group_tree_dn: Optional[str]
-    ldap_use_starttls: Optional[bool]
-    ldap_tls_cacert_base64: Optional[str]
-    ldap_tls_req_cert: Optional[str]
diff --git a/installers/charm/osm-keystone/src/interfaces.py b/installers/charm/osm-keystone/src/interfaces.py
deleted file mode 100644 (file)
index 7b019dd..0000000
+++ /dev/null
@@ -1,190 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Actions tab on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
-
-"""Interfaces used by this charm."""
-
-import ops.charm
-import ops.framework
-import ops.model
-
-
-class BaseRelationClient(ops.framework.Object):
-    """Requires side of a Kafka Endpoint."""
-
-    def __init__(
-        self,
-        charm: ops.charm.CharmBase,
-        relation_name: str,
-        mandatory_fields: list = [],
-    ):
-        super().__init__(charm, relation_name)
-        self.relation_name = relation_name
-        self.mandatory_fields = mandatory_fields
-        self._update_relation()
-
-    def get_data_from_unit(self, key: str):
-        """Get data from unit relation data."""
-        if not self.relation:
-            # This update relation doesn't seem to be needed, but I added it because apparently
-            # the data is empty in the unit tests.
-            # In reality, the constructor is called in every hook.
-            # In the unit tests when doing an update_relation_data, apparently it is not called.
-            self._update_relation()
-        if self.relation:
-            for unit in self.relation.units:
-                data = self.relation.data[unit].get(key)
-                if data:
-                    return data
-
-    def get_data_from_app(self, key: str):
-        """Get data from app relation data."""
-        if not self.relation or self.relation.app not in self.relation.data:
-            # This update relation doesn't seem to be needed, but I added it because apparently
-            # the data is empty in the unit tests.
-            # In reality, the constructor is called in every hook.
-            # In the unit tests when doing an update_relation_data, apparently it is not called.
-            self._update_relation()
-        if self.relation and self.relation.app in self.relation.data:
-            data = self.relation.data[self.relation.app].get(key)
-            if data:
-                return data
-
-    def is_missing_data_in_unit(self):
-        """Check if mandatory fields are present in any of the unit's relation data."""
-        return not all([self.get_data_from_unit(field) for field in self.mandatory_fields])
-
-    def is_missing_data_in_app(self):
-        """Check if mandatory fields are set in relation data."""
-        return not all([self.get_data_from_app(field) for field in self.mandatory_fields])
-
-    def _update_relation(self):
-        self.relation = self.framework.model.get_relation(self.relation_name)
-
-
-class MysqlClient(BaseRelationClient):
-    """Requires side of a Mysql Endpoint."""
-
-    mandatory_fields = ["host", "port", "user", "password", "root_password"]
-
-    def __init__(self, charm: ops.charm.CharmBase, relation_name: str):
-        super().__init__(charm, relation_name, self.mandatory_fields)
-
-    @property
-    def host(self):
-        """Host."""
-        return self.get_data_from_unit("host")
-
-    @property
-    def port(self):
-        """Port."""
-        return self.get_data_from_unit("port")
-
-    @property
-    def user(self):
-        """User."""
-        return self.get_data_from_unit("user")
-
-    @property
-    def password(self):
-        """Password."""
-        return self.get_data_from_unit("password")
-
-    @property
-    def root_password(self):
-        """Root password."""
-        return self.get_data_from_unit("root_password")
-
-    @property
-    def database(self):
-        """Database."""
-        return self.get_data_from_unit("database")
-
-    def get_root_uri(self, database: str):
-        """Get the URI for the mysql connection with the root user credentials.
-
-        Args:
-            database: Database name
-
-        Return:
-            A string with the following format:
-                mysql://root:<root_password>@<mysql_host>:<mysql_port>/<database>
-        """
-        return "mysql://root:{}@{}:{}/{}".format(
-            self.root_password, self.host, self.port, database
-        )
-
-    def get_uri(self):
-        """Get the URI for the mysql connection with the standard user credentials.
-
-        Args:
-            database: Database name
-        Return:
-            A string with the following format:
-                    mysql://<user>:<password>@<mysql_host>:<mysql_port>/<database>
-        """
-        return "mysql://{}:{}@{}:{}/{}".format(
-            self.user, self.password, self.host, self.port, self.database
-        )
-
-
-class KeystoneServer(ops.framework.Object):
-    """Provides side of a Keystone Endpoint."""
-
-    relation_name: str = None
-
-    def __init__(self, charm: ops.charm.CharmBase, relation_name: str):
-        super().__init__(charm, relation_name)
-        self.relation_name = relation_name
-
-    def publish_info(
-        self,
-        host: str,
-        port: int,
-        user_domain_name: str,
-        project_domain_name: str,
-        username: str,
-        password: str,
-        service: str,
-        keystone_db_password: str,
-        region_id: str,
-        admin_username: str,
-        admin_password: str,
-        admin_project_name: str,
-    ):
-        """Publish information in Keystone relation."""
-        if self.framework.model.unit.is_leader():
-            for relation in self.framework.model.relations[self.relation_name]:
-                relation_data = relation.data[self.framework.model.app]
-                relation_data["host"] = str(host)
-                relation_data["port"] = str(port)
-                relation_data["user_domain_name"] = str(user_domain_name)
-                relation_data["project_domain_name"] = str(project_domain_name)
-                relation_data["username"] = str(username)
-                relation_data["password"] = str(password)
-                relation_data["service"] = str(service)
-                relation_data["keystone_db_password"] = str(keystone_db_password)
-                relation_data["region_id"] = str(region_id)
-                relation_data["admin_username"] = str(admin_username)
-                relation_data["admin_password"] = str(admin_password)
-                relation_data["admin_project_name"] = str(admin_project_name)
diff --git a/installers/charm/osm-keystone/tests/integration/test_charm.py b/installers/charm/osm-keystone/tests/integration/test_charm.py
deleted file mode 100644 (file)
index 7e98542..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-
-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-under-test 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"})
-    await ops_test.model.deploy("charmed-osm-mariadb-k8s", application_name="mariadb-k8s")
-    # build and deploy charm from local source folder
-    charm = await ops_test.build_charm(".")
-    resources = {
-        "keystone-image": METADATA["resources"]["keystone-image"]["upstream-source"],
-    }
-    await ops_test.model.deploy(charm, resources=resources, application_name="keystone")
-    await ops_test.model.add_relation("keystone:db", "mariadb-k8s:mysql")
-    await ops_test.model.wait_for_idle(
-        apps=["keystone", "mariadb-k8s"], status="active", timeout=1000
-    )
-    assert ops_test.model.applications["keystone"].units[0].workload_status == "active"
-
-    await ops_test.model.set_config({"update-status-hook-interval": "60m"})
diff --git a/installers/charm/osm-keystone/tests/unit/test_charm.py b/installers/charm/osm-keystone/tests/unit/test_charm.py
deleted file mode 100644 (file)
index 7207b63..0000000
+++ /dev/null
@@ -1,136 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-
-import pytest
-from ops import pebble
-from ops.model import ActiveStatus, BlockedStatus
-from ops.testing import Harness
-from pytest_mock import MockerFixture
-
-from charm import FERNET_KEY_REPOSITORY, KEYSTONE_FOLDER, KeystoneCharm
-
-
-@pytest.fixture
-def harness_no_relations(mocker: MockerFixture):
-    mocker.patch("charm.cluster")
-    mocker.patch("charm.KubernetesServicePatch")
-    keystone_harness = Harness(KeystoneCharm)
-    keystone_harness.begin()
-    container = keystone_harness.charm.unit.get_container("keystone")
-    keystone_harness.set_can_connect(container, True)
-    container.make_dir(KEYSTONE_FOLDER, make_parents=True)
-    container.make_dir(FERNET_KEY_REPOSITORY, make_parents=True)
-    container.push(f"{FERNET_KEY_REPOSITORY}0", "token")
-    container.make_dir("/app", make_parents=True)
-    container.push("/app/start.sh", "")
-    container.exec = mocker.Mock()
-    yield keystone_harness
-    keystone_harness.cleanup()
-
-
-@pytest.fixture
-def harness(harness_no_relations: Harness):
-    mysql_rel_id = harness_no_relations.add_relation("db", "mysql")
-    harness_no_relations.add_relation_unit(mysql_rel_id, "mysql/0")
-    harness_no_relations.update_relation_data(
-        mysql_rel_id,
-        "mysql/0",
-        {
-            "host": "host",
-            "port": "3306",
-            "user": "user",
-            "root_password": "root_pass",
-            "password": "password",
-            "database": "db",
-        },
-    )
-    return harness_no_relations
-
-
-def test_mysql_missing_relation(mocker: MockerFixture, harness_no_relations: Harness):
-    spy_safe_restart = mocker.spy(harness_no_relations.charm, "_safe_restart")
-    harness_no_relations.charm.on.keystone_pebble_ready.emit("keystone")
-    assert harness_no_relations.charm.unit.status == BlockedStatus("mysql relation is missing")
-    assert spy_safe_restart.call_count == 1
-    harness_no_relations.charm.on.config_changed.emit()
-    assert harness_no_relations.charm.unit.status == BlockedStatus("mysql relation is missing")
-    assert spy_safe_restart.call_count == 2
-
-
-def test_mysql_relation_ready(mocker: MockerFixture, harness: Harness):
-    spy = mocker.spy(harness.charm, "_safe_restart")
-    harness.charm.on.config_changed.emit()
-    assert harness.charm.unit.status == ActiveStatus()
-    assert spy.call_count == 1
-
-
-def test_db_sync_action(mocker: MockerFixture, harness: Harness):
-    event_mock = mocker.Mock()
-    harness.charm._on_db_sync_action(event_mock)
-    event_mock.set_results.assert_called_once_with(
-        {"output": "db-sync was successfully executed."}
-    )
-    event_mock.fail.assert_not_called()
-    harness.charm.container.exec().wait.side_effect = pebble.ExecError(
-        ["keystone-manage", "db_sync"], 1, "", "Error"
-    )
-    harness.charm._on_db_sync_action(event_mock)
-    event_mock.fail.assert_called_once_with("db-sync action failed with code 1 and stderr Error.")
-
-
-def test_provide_keystone_relation(mocker: MockerFixture, harness: Harness):
-    # Non-leader
-    mon_rel_id = harness.add_relation("keystone", "mon")
-    harness.add_relation_unit(mon_rel_id, "mon/0")
-    data = harness.get_relation_data(mon_rel_id, harness.charm.app)
-    assert data == {}
-    # Leader
-    harness.set_leader(True)
-    nbi_rel_id = harness.add_relation("keystone", "nbi")
-    harness.add_relation_unit(nbi_rel_id, "nbi/0")
-    data = harness.get_relation_data(nbi_rel_id, harness.charm.app)
-    assert data == {
-        "host": "http://osm-keystone:5000/v3",
-        "port": "5000",
-        "user_domain_name": "default",
-        "project_domain_name": "default",
-        "username": "nbi",
-        "password": "nbi",
-        "service": "service",
-        "keystone_db_password": "admin",
-        "region_id": "RegionOne",
-        "admin_username": "admin",
-        "admin_password": "admin",
-        "admin_project_name": "admin",
-    }
-
-
-def test_update_status_rotation(mocker: MockerFixture, harness: Harness):
-    spy_fernet_rotate = mocker.spy(harness.charm, "_fernet_rotate")
-    harness.set_leader(True)
-    harness._update_config({"token-expiration": -1})
-    harness.charm.on.update_status.emit()
-    assert spy_fernet_rotate.call_count == 1
-
-
-def test_update_status_no_rotation(mocker: MockerFixture, harness: Harness):
-    spy_fernet_rotate = mocker.spy(harness.charm, "_fernet_rotate")
-    harness.set_leader(True)
-    harness._update_config({"token-expiration": 3600})
-    harness.charm.on.update_status.emit()
-    assert spy_fernet_rotate.call_count == 0
diff --git a/installers/charm/osm-keystone/tox.ini b/installers/charm/osm-keystone/tox.ini
deleted file mode 100644 (file)
index d08fe86..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-# 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.
-#
-
-[tox]
-skipsdist=True
-skip_missing_interpreters = True
-envlist = lint, unit, analyze, integration
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3.8
-setenv =
-  PYTHONPATH = {toxinidir}:{toxinidir}/lib:{[vars]src_path}
-  PYTHONBREAKPOINT=ipdb.set_trace
-passenv =
-  PYTHONPATH
-  HOME
-  PATH
-  CHARM_BUILD_DIR
-  MODEL_SETTINGS
-  HTTP_PROXY
-  HTTPS_PROXY
-  NO_PROXY
-
-[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
-    # prospector[with_everything]
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-    yamllint
-commands =
-    codespell {toxinidir}/*.yaml {toxinidir}/*.ini {toxinidir}/*.md \
-      {toxinidir}/*.toml {toxinidir}/*.txt {toxinidir}/.github
-    # prospector -A -F -T
-    yamllint -d '\{extends: default, ignore: "build\n.tox" \}' .
-    # 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
-    pytest-cov
-    coverage[toml]
-    -r{toxinidir}/requirements.txt
-commands =
-    pytest --ignore={[vars]tst_path}integration --cov={[vars]src_path} --cov-report=xml
-    coverage report --omit=tests/*
-
-[testenv:analyze]
-description = Run analize
-deps =
-    pylint==2.10.2
-    -r{toxinidir}/requirements.txt
-commands =
-    pylint -E {[vars]src_path}
-
-[testenv:security]
-description = Run security tests
-deps =
-    bandit
-    safety
-commands =
-    bandit -r {[vars]src_path}
-    - safety check
-
-[testenv:integration]
-description = Run integration tests
-deps =
-    pytest
-    juju<3
-    pytest-operator
-    -r{toxinidir}/requirements.txt
-commands =
-    pytest -v --tb native --ignore={[vars]tst_path}unit --log-cli-level=INFO -s {posargs} --cloud microk8s
diff --git a/installers/charm/osm-lcm/.gitignore b/installers/charm/osm-lcm/.gitignore
deleted file mode 100644 (file)
index 87d0a58..0000000
+++ /dev/null
@@ -1,29 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-venv/
-build/
-*.charm
-.tox/
-.coverage
-coverage.xml
-__pycache__/
-*.py[cod]
-.vscode
\ No newline at end of file
diff --git a/installers/charm/osm-lcm/.jujuignore b/installers/charm/osm-lcm/.jujuignore
deleted file mode 100644 (file)
index 17c7a8b..0000000
+++ /dev/null
@@ -1,23 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-/venv
-*.py[cod]
-*.charm
diff --git a/installers/charm/osm-lcm/CONTRIBUTING.md b/installers/charm/osm-lcm/CONTRIBUTING.md
deleted file mode 100644 (file)
index d4fd8b9..0000000
+++ /dev/null
@@ -1,78 +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.
-
-For those usages not covered by the Apache License, Version 2.0 please
-contact: legal@canonical.com
-
-To get in touch with the maintainers, please contact:
-osm-charmers@lists.launchpad.net -->
-
-# Contributing
-
-## Overview
-
-This documents explains the processes and practices recommended for contributing enhancements to
-this operator.
-
-- Generally, before developing enhancements to this charm, you should consider [opening an issue
-  ](https://osm.etsi.org/bugzilla/enter_bug.cgi?product=OSM) explaining your use case. (Component=devops, version=master)
-- If you would like to chat with us about your use-cases or proposed implementation, you can reach
-  us at [OSM Juju public channel](https://opensourcemano.slack.com/archives/C027KJGPECA).
-- 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 gerrit patch onto
-  the `master` branch.
-
-## 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 dev
-# Enable DEBUG logging
-juju model-config logging-config="<root>=INFO;unit=DEBUG"
-# Deploy the charm
-juju deploy ./osm-lcm_ubuntu-22.04-amd64.charm \
-    --resource lcm-image=opensourcemano/lcm:testing-daily --series jammy
-```
diff --git a/installers/charm/osm-lcm/LICENSE b/installers/charm/osm-lcm/LICENSE
deleted file mode 100644 (file)
index 7e9d504..0000000
+++ /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 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.
diff --git a/installers/charm/osm-lcm/README.md b/installers/charm/osm-lcm/README.md
deleted file mode 100644 (file)
index b9b2f80..0000000
+++ /dev/null
@@ -1,43 +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.
-
-For those usages not covered by the Apache License, Version 2.0 please
-contact: legal@canonical.com
-
-To get in touch with the maintainers, please contact:
-osm-charmers@lists.launchpad.net -->
-
-<!-- 
-Avoid using this README file for information that is maintained or published elsewhere, e.g.:
-
-* metadata.yaml > published on Charmhub
-* documentation > published on (or linked to from) Charmhub
-* detailed contribution guide > documentation or CONTRIBUTING.md
-
-Use links instead. 
--->
-
-# OSM LCM
-
-Charmhub package name: osm-lcm
-More information: https://charmhub.io/osm-lcm
-
-## Other resources
-
-* [Read more](https://osm.etsi.org/docs/user-guide/latest/) 
-
-* [Contributing](https://osm.etsi.org/gitweb/?p=osm/devops.git;a=blob;f=installers/charm/osm-lcm/CONTRIBUTING.md)
-
-* See the [Juju SDK documentation](https://juju.is/docs/sdk) for more information about developing and improving charms.
-                                                           
diff --git a/installers/charm/osm-lcm/actions.yaml b/installers/charm/osm-lcm/actions.yaml
deleted file mode 100644 (file)
index 0d73468..0000000
+++ /dev/null
@@ -1,26 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Actions tab on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
-
-get-debug-mode-information:
-  description: Get information to debug the container
diff --git a/installers/charm/osm-lcm/charmcraft.yaml b/installers/charm/osm-lcm/charmcraft.yaml
deleted file mode 100644 (file)
index f5e3ff3..0000000
+++ /dev/null
@@ -1,36 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-
-type: charm
-bases:
-  - build-on:
-      - name: "ubuntu"
-        channel: "22.04"
-    run-on:
-      - name: "ubuntu"
-        channel: "22.04"
-
-parts:
-  charm:
-    # build-packages:
-    #   - git
-    prime:
-      - files/*
diff --git a/installers/charm/osm-lcm/config.yaml b/installers/charm/osm-lcm/config.yaml
deleted file mode 100644 (file)
index e539f7b..0000000
+++ /dev/null
@@ -1,104 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Configure tab on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
-
-options:
-  log-level:
-    default: "INFO"
-    description: |
-      Set the Logging Level.
-
-      Options:
-        - TRACE
-        - DEBUG
-        - INFO
-        - WARN
-        - ERROR
-        - FATAL
-    type: string
-  database-commonkey:
-    description: Database COMMON KEY
-    type: string
-    default: osm
-  # Helm options
-  helm-stable-repo-url:
-    description: Stable repository URL for Helm charts
-    type: string
-    default: https://charts.helm.sh/stable
-  helm-ca-certs:
-    description: CA certificates to validate access to Helm repository
-    type: string
-    default: ""
-  # Debug-mode options
-  debug-mode:
-    type: boolean
-    description: |
-      Great for OSM Developers! (Not recommended for production deployments)
-
-      This action activates the Debug Mode, which sets up the container to be ready for debugging.
-      As part of the setup, SSH is enabled and a VSCode workspace file is automatically populated.
-
-      After enabling the debug-mode, execute the following command to get the information you need
-      to start debugging:
-        `juju run-action <unit name> get-debug-mode-information --wait`
-
-      The previous command returns the command you need to execute, and the SSH password that was set.
-
-      See also:
-        - https://charmhub.io/osm-lcm/configure#lcm-hostpath
-        - https://charmhub.io/osm-lcm/configure#n2vc-hostpath
-        - https://charmhub.io/osm-lcm/configure#common-hostpath
-    default: false
-  lcm-hostpath:
-    type: string
-    description: |
-      Set this config to the local path of the LCM module to persist the changes done during the
-      debug-mode session.
-
-      Example:
-        $ git clone "https://osm.etsi.org/gerrit/osm/LCM" /home/ubuntu/LCM
-        $ juju config lcm lcm-hostpath=/home/ubuntu/LCM
-
-      This configuration only applies if option `debug-mode` is set to true.
-  n2vc-hostpath:
-    type: string
-    description: |
-      Set this config to the local path of the N2VC module to persist the changes done during the
-      debug-mode session.
-
-      Example:
-        $ git clone "https://osm.etsi.org/gerrit/osm/N2VC" /home/ubuntu/N2VC
-        $ juju config lcm n2vc-hostpath=/home/ubuntu/N2VC
-
-      This configuration only applies if option `debug-mode` is set to true.
-  common-hostpath:
-    type: string
-    description: |
-      Set this config to the local path of the common module to persist the changes done during the
-      debug-mode session.
-
-      Example:
-        $ git clone "https://osm.etsi.org/gerrit/osm/common" /home/ubuntu/common
-        $ juju config lcm common-hostpath=/home/ubuntu/common
-
-      This configuration only applies if option `debug-mode` is set to true.
diff --git a/installers/charm/osm-lcm/files/vscode-workspace.json b/installers/charm/osm-lcm/files/vscode-workspace.json
deleted file mode 100644 (file)
index f17b24d..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-{
-    "folders": [
-        {"path": "/usr/lib/python3/dist-packages/osm_lcm"},
-        {"path": "/usr/lib/python3/dist-packages/osm_common"},
-        {"path": "/usr/lib/python3/dist-packages/n2vc"}
-    ],
-    "settings": {},
-    "launch": {
-        "version": "0.2.0",
-        "configurations": [
-            {
-                "name": "LCM",
-                "type": "python",
-                "request": "launch",
-                "module": "osm_lcm.lcm",
-                "justMyCode": false,
-            }
-        ]
-    }
-}
\ No newline at end of file
diff --git a/installers/charm/osm-lcm/lib/charms/data_platform_libs/v0/data_interfaces.py b/installers/charm/osm-lcm/lib/charms/data_platform_libs/v0/data_interfaces.py
deleted file mode 100644 (file)
index b3da5aa..0000000
+++ /dev/null
@@ -1,1130 +0,0 @@
-# Copyright 2023 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.
-
-"""Library to manage the relation for the data-platform products.
-
-This library contains the Requires and Provides classes for handling the relation
-between an application and multiple managed application supported by the data-team:
-MySQL, Postgresql, MongoDB, Redis,  and Kakfa.
-
-### Database (MySQL, Postgresql, MongoDB, and Redis)
-
-#### Requires Charm
-This library is a uniform interface to a selection of common database
-metadata, with added custom events that add convenience to database management,
-and methods to consume the application related data.
-
-
-Following an example of using the DatabaseCreatedEvent, in the context of the
-application charm code:
-
-```python
-
-from charms.data_platform_libs.v0.data_interfaces import (
-    DatabaseCreatedEvent,
-    DatabaseRequires,
-)
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Charm events defined in the database requires charm library.
-        self.database = DatabaseRequires(self, relation_name="database", database_name="database")
-        self.framework.observe(self.database.on.database_created, self._on_database_created)
-
-    def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-
-        # Start application with rendered configuration
-        self._start_application(config_file)
-
-        # Set active status
-        self.unit.status = ActiveStatus("received database credentials")
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
--  database_created: event emitted when the requested database is created.
--  endpoints_changed: event emitted when the read/write endpoints of the database have changed.
--  read_only_endpoints_changed: event emitted when the read-only endpoints of the database
-  have changed. Event is not triggered if read/write endpoints changed too.
-
-If it is needed to connect multiple database clusters to the same relation endpoint
-the application charm can implement the same code as if it would connect to only
-one database cluster (like the above code example).
-
-To differentiate multiple clusters connected to the same relation endpoint
-the application charm can use the name of the remote application:
-
-```python
-
-def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-    # Get the remote app name of the cluster that triggered this event
-    cluster = event.relation.app.name
-```
-
-It is also possible to provide an alias for each different database cluster/relation.
-
-So, it is possible to differentiate the clusters in two ways.
-The first is to use the remote application name, i.e., `event.relation.app.name`, as above.
-
-The second way is to use different event handlers to handle each cluster events.
-The implementation would be something like the following code:
-
-```python
-
-from charms.data_platform_libs.v0.data_interfaces import (
-    DatabaseCreatedEvent,
-    DatabaseRequires,
-)
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Define the cluster aliases and one handler for each cluster database created event.
-        self.database = DatabaseRequires(
-            self,
-            relation_name="database",
-            database_name="database",
-            relations_aliases = ["cluster1", "cluster2"],
-        )
-        self.framework.observe(
-            self.database.on.cluster1_database_created, self._on_cluster1_database_created
-        )
-        self.framework.observe(
-            self.database.on.cluster2_database_created, self._on_cluster2_database_created
-        )
-
-    def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster1
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-    def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster2
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-```
-
-### Provider Charm
-
-Following an example of using the DatabaseRequestedEvent, in the context of the
-database charm code:
-
-```python
-from charms.data_platform_libs.v0.data_interfaces import DatabaseProvides
-
-class SampleCharm(CharmBase):
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        # Charm events defined in the database provides charm library.
-        self.provided_database = DatabaseProvides(self, relation_name="database")
-        self.framework.observe(self.provided_database.on.database_requested,
-            self._on_database_requested)
-        # Database generic helper
-        self.database = DatabaseHelper()
-
-    def _on_database_requested(self, event: DatabaseRequestedEvent) -> None:
-        # Handle the event triggered by a new database requested in the relation
-        # Retrieve the database name using the charm library.
-        db_name = event.database
-        # generate a new user credential
-        username = self.database.generate_user()
-        password = self.database.generate_password()
-        # set the credentials for the relation
-        self.provided_database.set_credentials(event.relation.id, username, password)
-        # set other variables for the relation event.set_tls("False")
-```
-As shown above, the library provides a custom event (database_requested) to handle
-the situation when an application charm requests a new database to be created.
-It's preferred to subscribe to this event instead of relation changed event to avoid
-creating a new database when other information other than a database name is
-exchanged in the relation databag.
-
-### Kafka
-
-This library is the interface to use and interact with the Kafka charm. This library contains
-custom events that add convenience to manage Kafka, and provides methods to consume the
-application related data.
-
-#### Requirer Charm
-
-```python
-
-from charms.data_platform_libs.v0.data_interfaces import (
-    BootstrapServerChangedEvent,
-    KafkaRequires,
-    TopicCreatedEvent,
-)
-
-class ApplicationCharm(CharmBase):
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.kafka = KafkaRequires(self, "kafka_client", "test-topic")
-        self.framework.observe(
-            self.kafka.on.bootstrap_server_changed, self._on_kafka_bootstrap_server_changed
-        )
-        self.framework.observe(
-            self.kafka.on.topic_created, self._on_kafka_topic_created
-        )
-
-    def _on_kafka_bootstrap_server_changed(self, event: BootstrapServerChangedEvent):
-        # Event triggered when a bootstrap server was changed for this application
-
-        new_bootstrap_server = event.bootstrap_server
-        ...
-
-    def _on_kafka_topic_created(self, event: TopicCreatedEvent):
-        # Event triggered when a topic was created for this application
-        username = event.username
-        password = event.password
-        tls = event.tls
-        tls_ca= event.tls_ca
-        bootstrap_server event.bootstrap_server
-        consumer_group_prefic = event.consumer_group_prefix
-        zookeeper_uris = event.zookeeper_uris
-        ...
-
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
-- topic_created: event emitted when the requested topic is created.
-- bootstrap_server_changed: event emitted when the bootstrap server have changed.
-- credential_changed: event emitted when the credentials of Kafka changed.
-
-### Provider Charm
-
-Following the previous example, this is an example of the provider charm.
-
-```python
-class SampleCharm(CharmBase):
-
-from charms.data_platform_libs.v0.data_interfaces import (
-    KafkaProvides,
-    TopicRequestedEvent,
-)
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Default charm events.
-        self.framework.observe(self.on.start, self._on_start)
-
-        # Charm events defined in the Kafka Provides charm library.
-        self.kafka_provider = KafkaProvides(self, relation_name="kafka_client")
-        self.framework.observe(self.kafka_provider.on.topic_requested, self._on_topic_requested)
-        # Kafka generic helper
-        self.kafka = KafkaHelper()
-
-    def _on_topic_requested(self, event: TopicRequestedEvent):
-        # Handle the on_topic_requested event.
-
-        topic = event.topic
-        relation_id = event.relation.id
-        # set connection info in the databag relation
-        self.kafka_provider.set_bootstrap_server(relation_id, self.kafka.get_bootstrap_server())
-        self.kafka_provider.set_credentials(relation_id, username=username, password=password)
-        self.kafka_provider.set_consumer_group_prefix(relation_id, ...)
-        self.kafka_provider.set_tls(relation_id, "False")
-        self.kafka_provider.set_zookeeper_uris(relation_id, ...)
-
-```
-As shown above, the library provides a custom event (topic_requested) to handle
-the situation when an application charm requests a new topic to be created.
-It is preferred to subscribe to this event instead of relation changed event to avoid
-creating a new topic when other information other than a topic name is
-exchanged in the relation databag.
-"""
-
-import json
-import logging
-from abc import ABC, abstractmethod
-from collections import namedtuple
-from datetime import datetime
-from typing import List, Optional
-
-from ops.charm import (
-    CharmBase,
-    CharmEvents,
-    RelationChangedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-)
-from ops.framework import EventSource, Object
-from ops.model import Relation
-
-# The unique Charmhub library identifier, never change it
-LIBID = "6c3e6b6680d64e9c89e611d1a15f65be"
-
-# 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 = 7
-
-PYDEPS = ["ops>=2.0.0"]
-
-logger = logging.getLogger(__name__)
-
-Diff = namedtuple("Diff", "added changed deleted")
-Diff.__doc__ = """
-A tuple for storing the diff between two data mappings.
-
-added - keys that were added
-changed - keys that still exist but have new values
-deleted - key that were deleted"""
-
-
-def diff(event: RelationChangedEvent, bucket: str) -> Diff:
-    """Retrieves the diff of the data in the relation changed databag.
-
-    Args:
-        event: relation changed event.
-        bucket: bucket of the databag (app or unit)
-
-    Returns:
-        a Diff instance containing the added, deleted and changed
-            keys from the event relation databag.
-    """
-    # Retrieve the old data from the data key in the application relation databag.
-    old_data = json.loads(event.relation.data[bucket].get("data", "{}"))
-    # Retrieve the new data from the event relation databag.
-    new_data = {
-        key: value for key, value in event.relation.data[event.app].items() if key != "data"
-    }
-
-    # These are the keys that were added to the databag and triggered this event.
-    added = new_data.keys() - old_data.keys()
-    # These are the keys that were removed from the databag and triggered this event.
-    deleted = old_data.keys() - new_data.keys()
-    # These are the keys that already existed in the databag,
-    # but had their values changed.
-    changed = {key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]}
-    # Convert the new_data to a serializable format and save it for a next diff check.
-    event.relation.data[bucket].update({"data": json.dumps(new_data)})
-
-    # Return the diff with all possible changes.
-    return Diff(added, changed, deleted)
-
-
-# Base DataProvides and DataRequires
-
-
-class DataProvides(Object, ABC):
-    """Base provides-side of the data products relation."""
-
-    def __init__(self, charm: CharmBase, relation_name: str) -> None:
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.framework.observe(
-            charm.on[relation_name].relation_changed,
-            self._on_relation_changed,
-        )
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        return diff(event, self.local_app)
-
-    @abstractmethod
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the relation data has changed."""
-        raise NotImplementedError
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation id).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = {
-                key: value for key, value in relation.data[relation.app].items() if key != "data"
-            }
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return list(self.charm.model.relations[self.relation_name])
-
-    def set_credentials(self, relation_id: int, username: str, password: str) -> None:
-        """Set credentials.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            username: user that was created.
-            password: password of the created user.
-        """
-        self._update_relation_data(
-            relation_id,
-            {
-                "username": username,
-                "password": password,
-            },
-        )
-
-    def set_tls(self, relation_id: int, tls: str) -> None:
-        """Set whether TLS is enabled.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            tls: whether tls is enabled (True or False).
-        """
-        self._update_relation_data(relation_id, {"tls": tls})
-
-    def set_tls_ca(self, relation_id: int, tls_ca: str) -> None:
-        """Set the TLS CA in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            tls_ca: TLS certification authority.
-        """
-        self._update_relation_data(relation_id, {"tls_ca": tls_ca})
-
-
-class DataRequires(Object, ABC):
-    """Requires-side of the relation."""
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        extra_user_roles: str = None,
-    ):
-        """Manager of base client relations."""
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.extra_user_roles = extra_user_roles
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined, self._on_relation_joined_event
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed, self._on_relation_changed_event
-        )
-
-    @abstractmethod
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the relation."""
-        raise NotImplementedError
-
-    @abstractmethod
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        raise NotImplementedError
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-        Function cannot be used in `*-relation-broken` events and will raise an exception.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation ID).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = {
-                key: value for key, value in relation.data[relation.app].items() if key != "data"
-            }
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        return diff(event, self.local_unit)
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return [
-            relation
-            for relation in self.charm.model.relations[self.relation_name]
-            if self._is_relation_active(relation)
-        ]
-
-    @staticmethod
-    def _is_relation_active(relation: Relation):
-        try:
-            _ = repr(relation.data)
-            return True
-        except RuntimeError:
-            return False
-
-    @staticmethod
-    def _is_resource_created_for_relation(relation: Relation):
-        return (
-            "username" in relation.data[relation.app] and "password" in relation.data[relation.app]
-        )
-
-    def is_resource_created(self, relation_id: Optional[int] = None) -> bool:
-        """Check if the resource has been created.
-
-        This function can be used to check if the Provider answered with data in the charm code
-        when outside an event callback.
-
-        Args:
-            relation_id (int, optional): When provided the check is done only for the relation id
-                provided, otherwise the check is done for all relations
-
-        Returns:
-            True or False
-
-        Raises:
-            IndexError: If relation_id is provided but that relation does not exist
-        """
-        if relation_id is not None:
-            try:
-                relation = [relation for relation in self.relations if relation.id == relation_id][
-                    0
-                ]
-                return self._is_resource_created_for_relation(relation)
-            except IndexError:
-                raise IndexError(f"relation id {relation_id} cannot be accessed")
-        else:
-            return (
-                all(
-                    [
-                        self._is_resource_created_for_relation(relation)
-                        for relation in self.relations
-                    ]
-                )
-                if self.relations
-                else False
-            )
-
-
-# General events
-
-
-class ExtraRoleEvent(RelationEvent):
-    """Base class for data events."""
-
-    @property
-    def extra_user_roles(self) -> Optional[str]:
-        """Returns the extra user roles that were requested."""
-        return self.relation.data[self.relation.app].get("extra-user-roles")
-
-
-class AuthenticationEvent(RelationEvent):
-    """Base class for authentication fields for events."""
-
-    @property
-    def username(self) -> Optional[str]:
-        """Returns the created username."""
-        return self.relation.data[self.relation.app].get("username")
-
-    @property
-    def password(self) -> Optional[str]:
-        """Returns the password for the created user."""
-        return self.relation.data[self.relation.app].get("password")
-
-    @property
-    def tls(self) -> Optional[str]:
-        """Returns whether TLS is configured."""
-        return self.relation.data[self.relation.app].get("tls")
-
-    @property
-    def tls_ca(self) -> Optional[str]:
-        """Returns TLS CA."""
-        return self.relation.data[self.relation.app].get("tls-ca")
-
-
-# Database related events and fields
-
-
-class DatabaseProvidesEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def database(self) -> Optional[str]:
-        """Returns the database that was requested."""
-        return self.relation.data[self.relation.app].get("database")
-
-
-class DatabaseRequestedEvent(DatabaseProvidesEvent, ExtraRoleEvent):
-    """Event emitted when a new database is requested for use on this relation."""
-
-
-class DatabaseProvidesEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_requested = EventSource(DatabaseRequestedEvent)
-
-
-class DatabaseRequiresEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read/write endpoints."""
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def read_only_endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read only endpoints."""
-        return self.relation.data[self.relation.app].get("read-only-endpoints")
-
-    @property
-    def replset(self) -> Optional[str]:
-        """Returns the replicaset name.
-
-        MongoDB only.
-        """
-        return self.relation.data[self.relation.app].get("replset")
-
-    @property
-    def uris(self) -> Optional[str]:
-        """Returns the connection URIs.
-
-        MongoDB, Redis, OpenSearch.
-        """
-        return self.relation.data[self.relation.app].get("uris")
-
-    @property
-    def version(self) -> Optional[str]:
-        """Returns the version of the database.
-
-        Version as informed by the database daemon.
-        """
-        return self.relation.data[self.relation.app].get("version")
-
-
-class DatabaseCreatedEvent(AuthenticationEvent, DatabaseRequiresEvent):
-    """Event emitted when a new database is created for use on this relation."""
-
-
-class DatabaseEndpointsChangedEvent(AuthenticationEvent, DatabaseRequiresEvent):
-    """Event emitted when the read/write endpoints are changed."""
-
-
-class DatabaseReadOnlyEndpointsChangedEvent(AuthenticationEvent, DatabaseRequiresEvent):
-    """Event emitted when the read only endpoints are changed."""
-
-
-class DatabaseRequiresEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_created = EventSource(DatabaseCreatedEvent)
-    endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
-    read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent)
-
-
-# Database Provider and Requires
-
-
-class DatabaseProvides(DataProvides):
-    """Provider-side of the database relations."""
-
-    on = DatabaseProvidesEvents()
-
-    def __init__(self, charm: CharmBase, relation_name: str) -> None:
-        super().__init__(charm, relation_name)
-
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the relation has changed."""
-        # Only the leader should handle this event.
-        if not self.local_unit.is_leader():
-            return
-
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Emit a database requested event if the setup key (database name and optional
-        # extra user roles) was added to the relation databag by the application.
-        if "database" in diff.added:
-            self.on.database_requested.emit(event.relation, app=event.app, unit=event.unit)
-
-    def set_endpoints(self, relation_id: int, connection_strings: str) -> None:
-        """Set database primary connections.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            connection_strings: database hosts and ports comma separated list.
-        """
-        self._update_relation_data(relation_id, {"endpoints": connection_strings})
-
-    def set_read_only_endpoints(self, relation_id: int, connection_strings: str) -> None:
-        """Set database replicas connection strings.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            connection_strings: database hosts and ports comma separated list.
-        """
-        self._update_relation_data(relation_id, {"read-only-endpoints": connection_strings})
-
-    def set_replset(self, relation_id: int, replset: str) -> None:
-        """Set replica set name in the application relation databag.
-
-        MongoDB only.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            replset: replica set name.
-        """
-        self._update_relation_data(relation_id, {"replset": replset})
-
-    def set_uris(self, relation_id: int, uris: str) -> None:
-        """Set the database connection URIs in the application relation databag.
-
-        MongoDB, Redis, and OpenSearch only.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            uris: connection URIs.
-        """
-        self._update_relation_data(relation_id, {"uris": uris})
-
-    def set_version(self, relation_id: int, version: str) -> None:
-        """Set the database version in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            version: database version.
-        """
-        self._update_relation_data(relation_id, {"version": version})
-
-
-class DatabaseRequires(DataRequires):
-    """Requires-side of the database relation."""
-
-    on = DatabaseRequiresEvents()
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        database_name: str,
-        extra_user_roles: str = None,
-        relations_aliases: List[str] = None,
-    ):
-        """Manager of database client relations."""
-        super().__init__(charm, relation_name, extra_user_roles)
-        self.database = database_name
-        self.relations_aliases = relations_aliases
-
-        # Define custom event names for each alias.
-        if relations_aliases:
-            # Ensure the number of aliases does not exceed the maximum
-            # of connections allowed in the specific relation.
-            relation_connection_limit = self.charm.meta.requires[relation_name].limit
-            if len(relations_aliases) != relation_connection_limit:
-                raise ValueError(
-                    f"The number of aliases must match the maximum number of connections allowed in the relation. "
-                    f"Expected {relation_connection_limit}, got {len(relations_aliases)}"
-                )
-
-            for relation_alias in relations_aliases:
-                self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent)
-                self.on.define_event(
-                    f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent
-                )
-                self.on.define_event(
-                    f"{relation_alias}_read_only_endpoints_changed",
-                    DatabaseReadOnlyEndpointsChangedEvent,
-                )
-
-    def _assign_relation_alias(self, relation_id: int) -> None:
-        """Assigns an alias to a relation.
-
-        This function writes in the unit data bag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-        """
-        # If no aliases were provided, return immediately.
-        if not self.relations_aliases:
-            return
-
-        # Return if an alias was already assigned to this relation
-        # (like when there are more than one unit joining the relation).
-        if (
-            self.charm.model.get_relation(self.relation_name, relation_id)
-            .data[self.local_unit]
-            .get("alias")
-        ):
-            return
-
-        # Retrieve the available aliases (the ones that weren't assigned to any relation).
-        available_aliases = self.relations_aliases[:]
-        for relation in self.charm.model.relations[self.relation_name]:
-            alias = relation.data[self.local_unit].get("alias")
-            if alias:
-                logger.debug("Alias %s was already assigned to relation %d", alias, relation.id)
-                available_aliases.remove(alias)
-
-        # Set the alias in the unit relation databag of the specific relation.
-        relation = self.charm.model.get_relation(self.relation_name, relation_id)
-        relation.data[self.local_unit].update({"alias": available_aliases[0]})
-
-    def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None:
-        """Emit an aliased event to a particular relation if it has an alias.
-
-        Args:
-            event: the relation changed event that was received.
-            event_name: the name of the event to emit.
-        """
-        alias = self._get_relation_alias(event.relation.id)
-        if alias:
-            getattr(self.on, f"{alias}_{event_name}").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-    def _get_relation_alias(self, relation_id: int) -> Optional[str]:
-        """Returns the relation alias.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-
-        Returns:
-            the relation alias or None if the relation was not found.
-        """
-        for relation in self.charm.model.relations[self.relation_name]:
-            if relation.id == relation_id:
-                return relation.data[self.local_unit].get("alias")
-        return None
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the database relation."""
-        # If relations aliases were provided, assign one to the relation.
-        self._assign_relation_alias(event.relation.id)
-
-        # Sets both database and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the database.
-        if self.extra_user_roles:
-            self._update_relation_data(
-                event.relation.id,
-                {
-                    "database": self.database,
-                    "extra-user-roles": self.extra_user_roles,
-                },
-            )
-        else:
-            self._update_relation_data(event.relation.id, {"database": self.database})
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the database relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the database is created
-        # (the database charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("database created at %s", datetime.now())
-            self.on.database_created.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "database_created")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “database_created“ is triggered.
-            return
-
-        # Emit an endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "endpoints_changed")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered.
-            return
-
-        # Emit a read only endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("read-only-endpoints changed on %s", datetime.now())
-            self.on.read_only_endpoints_changed.emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "read_only_endpoints_changed")
-
-
-# Kafka related events
-
-
-class KafkaProvidesEvent(RelationEvent):
-    """Base class for Kafka events."""
-
-    @property
-    def topic(self) -> Optional[str]:
-        """Returns the topic that was requested."""
-        return self.relation.data[self.relation.app].get("topic")
-
-
-class TopicRequestedEvent(KafkaProvidesEvent, ExtraRoleEvent):
-    """Event emitted when a new topic is requested for use on this relation."""
-
-
-class KafkaProvidesEvents(CharmEvents):
-    """Kafka events.
-
-    This class defines the events that the Kafka can emit.
-    """
-
-    topic_requested = EventSource(TopicRequestedEvent)
-
-
-class KafkaRequiresEvent(RelationEvent):
-    """Base class for Kafka events."""
-
-    @property
-    def bootstrap_server(self) -> Optional[str]:
-        """Returns a a comma-seperated list of broker uris."""
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def consumer_group_prefix(self) -> Optional[str]:
-        """Returns the consumer-group-prefix."""
-        return self.relation.data[self.relation.app].get("consumer-group-prefix")
-
-    @property
-    def zookeeper_uris(self) -> Optional[str]:
-        """Returns a comma separated list of Zookeeper uris."""
-        return self.relation.data[self.relation.app].get("zookeeper-uris")
-
-
-class TopicCreatedEvent(AuthenticationEvent, KafkaRequiresEvent):
-    """Event emitted when a new topic is created for use on this relation."""
-
-
-class BootstrapServerChangedEvent(AuthenticationEvent, KafkaRequiresEvent):
-    """Event emitted when the bootstrap server is changed."""
-
-
-class KafkaRequiresEvents(CharmEvents):
-    """Kafka events.
-
-    This class defines the events that the Kafka can emit.
-    """
-
-    topic_created = EventSource(TopicCreatedEvent)
-    bootstrap_server_changed = EventSource(BootstrapServerChangedEvent)
-
-
-# Kafka Provides and Requires
-
-
-class KafkaProvides(DataProvides):
-    """Provider-side of the Kafka relation."""
-
-    on = KafkaProvidesEvents()
-
-    def __init__(self, charm: CharmBase, relation_name: str) -> None:
-        super().__init__(charm, relation_name)
-
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the relation has changed."""
-        # Only the leader should handle this event.
-        if not self.local_unit.is_leader():
-            return
-
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Emit a topic requested event if the setup key (topic name and optional
-        # extra user roles) was added to the relation databag by the application.
-        if "topic" in diff.added:
-            self.on.topic_requested.emit(event.relation, app=event.app, unit=event.unit)
-
-    def set_bootstrap_server(self, relation_id: int, bootstrap_server: str) -> None:
-        """Set the bootstrap server in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            bootstrap_server: the bootstrap server address.
-        """
-        self._update_relation_data(relation_id, {"endpoints": bootstrap_server})
-
-    def set_consumer_group_prefix(self, relation_id: int, consumer_group_prefix: str) -> None:
-        """Set the consumer group prefix in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            consumer_group_prefix: the consumer group prefix string.
-        """
-        self._update_relation_data(relation_id, {"consumer-group-prefix": consumer_group_prefix})
-
-    def set_zookeeper_uris(self, relation_id: int, zookeeper_uris: str) -> None:
-        """Set the zookeeper uris in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            zookeeper_uris: comma-seperated list of ZooKeeper server uris.
-        """
-        self._update_relation_data(relation_id, {"zookeeper-uris": zookeeper_uris})
-
-
-class KafkaRequires(DataRequires):
-    """Requires-side of the Kafka relation."""
-
-    on = KafkaRequiresEvents()
-
-    def __init__(self, charm, relation_name: str, topic: str, extra_user_roles: str = None):
-        """Manager of Kafka client relations."""
-        # super().__init__(charm, relation_name)
-        super().__init__(charm, relation_name, extra_user_roles)
-        self.charm = charm
-        self.topic = topic
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the Kafka relation."""
-        # Sets both topic and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the topic.
-        self._update_relation_data(
-            event.relation.id,
-            {
-                "topic": self.topic,
-                "extra-user-roles": self.extra_user_roles,
-            }
-            if self.extra_user_roles is not None
-            else {"topic": self.topic},
-        )
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the Kafka relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the topic is created
-        # (the Kafka charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("topic created at %s", datetime.now())
-            self.on.topic_created.emit(event.relation, app=event.app, unit=event.unit)
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “topic_created“ is triggered.
-            return
-
-        # Emit an endpoints (bootstap-server) changed event if the Kakfa endpoints
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            self.on.bootstrap_server_changed.emit(
-                event.relation, app=event.app, unit=event.unit
-            )  # here check if this is the right design
-            return
diff --git a/installers/charm/osm-lcm/lib/charms/kafka_k8s/v0/kafka.py b/installers/charm/osm-lcm/lib/charms/kafka_k8s/v0/kafka.py
deleted file mode 100644 (file)
index aeb5edc..0000000
+++ /dev/null
@@ -1,200 +0,0 @@
-# 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.
-
-"""Kafka library.
-
-This [library](https://juju.is/docs/sdk/libraries) implements both sides of the
-`kafka` [interface](https://juju.is/docs/sdk/relations).
-
-The *provider* side of this interface is implemented by the
-[kafka-k8s Charmed Operator](https://charmhub.io/kafka-k8s).
-
-Any Charmed Operator that *requires* Kafka for providing its
-service should implement the *requirer* side of this interface.
-
-In a nutshell using this library to implement a Charmed Operator *requiring*
-Kafka would look like
-
-```
-$ charmcraft fetch-lib charms.kafka_k8s.v0.kafka
-```
-
-`metadata.yaml`:
-
-```
-requires:
-  kafka:
-    interface: kafka
-    limit: 1
-```
-
-`src/charm.py`:
-
-```
-from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
-from ops.charm import CharmBase
-
-
-class MyCharm(CharmBase):
-
-    on = KafkaEvents()
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.kafka = KafkaRequires(self)
-        self.framework.observe(
-            self.on.kafka_available,
-            self._on_kafka_available,
-        )
-        self.framework.observe(
-            self.on["kafka"].relation_broken,
-            self._on_kafka_broken,
-        )
-
-    def _on_kafka_available(self, event):
-        # Get Kafka host and port
-        host: str = self.kafka.host
-        port: int = self.kafka.port
-        # host => "kafka-k8s"
-        # port => 9092
-
-    def _on_kafka_broken(self, event):
-        # Stop service
-        # ...
-        self.unit.status = BlockedStatus("need kafka relation")
-```
-
-You can file bugs
-[here](https://github.com/charmed-osm/kafka-k8s-operator/issues)!
-"""
-
-from typing import Optional
-
-from ops.charm import CharmBase, CharmEvents
-from ops.framework import EventBase, EventSource, Object
-
-# The unique Charmhub library identifier, never change it
-from ops.model import Relation
-
-LIBID = "eacc8c85082347c9aae740e0220b8376"
-
-# 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 = 4
-
-
-KAFKA_HOST_APP_KEY = "host"
-KAFKA_PORT_APP_KEY = "port"
-
-
-class _KafkaAvailableEvent(EventBase):
-    """Event emitted when Kafka is available."""
-
-
-class KafkaEvents(CharmEvents):
-    """Kafka events.
-
-    This class defines the events that Kafka can emit.
-
-    Events:
-        kafka_available (_KafkaAvailableEvent)
-    """
-
-    kafka_available = EventSource(_KafkaAvailableEvent)
-
-
-class KafkaRequires(Object):
-    """Requires-side of the Kafka relation."""
-
-    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
-        super().__init__(charm, endpoint_name)
-        self.charm = charm
-        self._endpoint_name = endpoint_name
-
-        # Observe relation events
-        event_observe_mapping = {
-            charm.on[self._endpoint_name].relation_changed: self._on_relation_changed,
-        }
-        for event, observer in event_observe_mapping.items():
-            self.framework.observe(event, observer)
-
-    def _on_relation_changed(self, event) -> None:
-        if event.relation.app and all(
-            key in event.relation.data[event.relation.app]
-            for key in (KAFKA_HOST_APP_KEY, KAFKA_PORT_APP_KEY)
-        ):
-            self.charm.on.kafka_available.emit()
-
-    @property
-    def host(self) -> str:
-        """Get kafka hostname."""
-        relation: Relation = self.model.get_relation(self._endpoint_name)
-        return (
-            relation.data[relation.app].get(KAFKA_HOST_APP_KEY)
-            if relation and relation.app
-            else None
-        )
-
-    @property
-    def port(self) -> int:
-        """Get kafka port number."""
-        relation: Relation = self.model.get_relation(self._endpoint_name)
-        return (
-            int(relation.data[relation.app].get(KAFKA_PORT_APP_KEY))
-            if relation and relation.app
-            else None
-        )
-
-
-class KafkaProvides(Object):
-    """Provides-side of the Kafka relation."""
-
-    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
-        super().__init__(charm, endpoint_name)
-        self._endpoint_name = endpoint_name
-
-    def set_host_info(self, host: str, port: int, relation: Optional[Relation] = None) -> None:
-        """Set Kafka host and port.
-
-        This function writes in the application data of the relation, therefore,
-        only the unit leader can call it.
-
-        Args:
-            host (str): Kafka hostname or IP address.
-            port (int): Kafka port.
-            relation (Optional[Relation]): Relation to update.
-                                           If not specified, all relations will be updated.
-
-        Raises:
-            Exception: if a non-leader unit calls this function.
-        """
-        if not self.model.unit.is_leader():
-            raise Exception("only the leader set host information.")
-
-        if relation:
-            self._update_relation_data(host, port, relation)
-            return
-
-        for relation in self.model.relations[self._endpoint_name]:
-            self._update_relation_data(host, port, relation)
-
-    def _update_relation_data(self, host: str, port: int, relation: Relation) -> None:
-        """Update data in relation if needed."""
-        relation.data[self.model.app][KAFKA_HOST_APP_KEY] = host
-        relation.data[self.model.app][KAFKA_PORT_APP_KEY] = str(port)
diff --git a/installers/charm/osm-lcm/lib/charms/osm_libs/v0/utils.py b/installers/charm/osm-lcm/lib/charms/osm_libs/v0/utils.py
deleted file mode 100644 (file)
index d739ba6..0000000
+++ /dev/null
@@ -1,544 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-#         http://www.apache.org/licenses/LICENSE-2.0
-"""OSM Utils Library.
-
-This library offers some utilities made for but not limited to Charmed OSM.
-
-# Getting started
-
-Execute the following command inside your Charmed Operator folder to fetch the library.
-
-```shell
-charmcraft fetch-lib charms.osm_libs.v0.utils
-```
-
-# CharmError Exception
-
-An exception that takes to arguments, the message and the StatusBase class, which are useful
-to set the status of the charm when the exception raises.
-
-Example:
-```shell
-from charms.osm_libs.v0.utils import CharmError
-
-class MyCharm(CharmBase):
-    def _on_config_changed(self, _):
-        try:
-            if not self.config.get("some-option"):
-                raise CharmError("need some-option", BlockedStatus)
-
-            if not self.mysql_ready:
-                raise CharmError("waiting for mysql", WaitingStatus)
-
-            # Do stuff...
-
-        exception CharmError as e:
-            self.unit.status = e.status
-```
-
-# Pebble validations
-
-The `check_container_ready` function checks that a container is ready,
-and therefore Pebble is ready.
-
-The `check_service_active` function checks that a service in a container is running.
-
-Both functions raise a CharmError if the validations fail.
-
-Example:
-```shell
-from charms.osm_libs.v0.utils import check_container_ready, check_service_active
-
-class MyCharm(CharmBase):
-    def _on_config_changed(self, _):
-        try:
-            container: Container = self.unit.get_container("my-container")
-            check_container_ready(container)
-            check_service_active(container, "my-service")
-            # Do stuff...
-
-        exception CharmError as e:
-            self.unit.status = e.status
-```
-
-# Debug-mode
-
-The debug-mode allows OSM developers to easily debug OSM modules.
-
-Example:
-```shell
-from charms.osm_libs.v0.utils import DebugMode
-
-class MyCharm(CharmBase):
-    _stored = StoredState()
-
-    def __init__(self, _):
-        # ...
-        container: Container = self.unit.get_container("my-container")
-        hostpaths = [
-            HostPath(
-                config="module-hostpath",
-                container_path="/usr/lib/python3/dist-packages/module"
-            ),
-        ]
-        vscode_workspace_path = "files/vscode-workspace.json"
-        self.debug_mode = DebugMode(
-            self,
-            self._stored,
-            container,
-            hostpaths,
-            vscode_workspace_path,
-        )
-
-    def _on_update_status(self, _):
-        if self.debug_mode.started:
-            return
-        # ...
-
-    def _get_debug_mode_information(self):
-        command = self.debug_mode.command
-        password = self.debug_mode.password
-        return command, password
-```
-
-# More
-
-- Get pod IP with `get_pod_ip()`
-"""
-from dataclasses import dataclass
-import logging
-import secrets
-import socket
-from pathlib import Path
-from typing import List
-
-from lightkube import Client
-from lightkube.models.core_v1 import HostPathVolumeSource, Volume, VolumeMount
-from lightkube.resources.apps_v1 import StatefulSet
-from ops.charm import CharmBase
-from ops.framework import Object, StoredState
-from ops.model import (
-    ActiveStatus,
-    BlockedStatus,
-    Container,
-    MaintenanceStatus,
-    StatusBase,
-    WaitingStatus,
-)
-from ops.pebble import ServiceStatus
-
-# The unique Charmhub library identifier, never change it
-LIBID = "e915908eebee4cdd972d484728adf984"
-
-# 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 = 5
-
-logger = logging.getLogger(__name__)
-
-
-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)
-
-
-def check_container_ready(container: Container) -> None:
-    """Check Pebble has started in the container.
-
-    Args:
-        container (Container): Container to be checked.
-
-    Raises:
-        CharmError: if container is not ready.
-    """
-    if not container.can_connect():
-        raise CharmError("waiting for pebble to start", MaintenanceStatus)
-
-
-def check_service_active(container: Container, service_name: str) -> None:
-    """Check if the service is running.
-
-    Args:
-        container (Container): Container to be checked.
-        service_name (str): Name of the service to check.
-
-    Raises:
-        CharmError: if the service is not running.
-    """
-    if service_name not in container.get_plan().services:
-        raise CharmError(f"{service_name} service not configured yet", WaitingStatus)
-
-    if container.get_service(service_name).current != ServiceStatus.ACTIVE:
-        raise CharmError(f"{service_name} service is not running")
-
-
-def get_pod_ip() -> str:
-    """Get Kubernetes Pod IP.
-
-    Returns:
-        str: The IP of the Pod.
-    """
-    return socket.gethostbyname(socket.gethostname())
-
-
-_DEBUG_SCRIPT = r"""#!/bin/bash
-# Install SSH
-
-function download_code(){{
-    wget https://go.microsoft.com/fwlink/?LinkID=760868 -O code.deb
-}}
-
-function setup_envs(){{
-    grep "source /debug.envs" /root/.bashrc || echo "source /debug.envs" | tee -a /root/.bashrc
-}}
-function setup_ssh(){{
-    apt install ssh -y
-    cat /etc/ssh/sshd_config |
-        grep -E '^PermitRootLogin yes$$' || (
-        echo PermitRootLogin yes |
-        tee -a /etc/ssh/sshd_config
-    )
-    service ssh stop
-    sleep 3
-    service ssh start
-    usermod --password $(echo {} | openssl passwd -1 -stdin) root
-}}
-
-function setup_code(){{
-    apt install libasound2 -y
-    (dpkg -i code.deb || apt-get install -f -y || apt-get install -f -y) && echo Code installed successfully
-    code --install-extension ms-python.python --user-data-dir /root
-    mkdir -p /root/.vscode-server
-    cp -R /root/.vscode/extensions /root/.vscode-server/extensions
-}}
-
-export DEBIAN_FRONTEND=noninteractive
-apt update && apt install wget -y
-download_code &
-setup_ssh &
-setup_envs
-wait
-setup_code &
-wait
-"""
-
-
-@dataclass
-class SubModule:
-    """Represent RO Submodules."""
-    sub_module_path: str
-    container_path: str
-
-
-class HostPath:
-    """Represents a hostpath."""
-    def __init__(self, config: str, container_path: str, submodules: dict = None) -> None:
-        mount_path_items = config.split("-")
-        mount_path_items.reverse()
-        self.mount_path = "/" + "/".join(mount_path_items)
-        self.config = config
-        self.sub_module_dict = {}
-        if submodules:
-            for submodule in submodules.keys():
-                self.sub_module_dict[submodule] = SubModule(
-                    sub_module_path=self.mount_path + "/" + submodule + "/" + submodules[submodule].split("/")[-1],
-                    container_path=submodules[submodule],
-                )
-        else:
-            self.container_path = container_path
-            self.module_name = container_path.split("/")[-1]
-
-class DebugMode(Object):
-    """Class to handle the debug-mode."""
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        stored: StoredState,
-        container: Container,
-        hostpaths: List[HostPath] = [],
-        vscode_workspace_path: str = "files/vscode-workspace.json",
-    ) -> None:
-        super().__init__(charm, "debug-mode")
-
-        self.charm = charm
-        self._stored = stored
-        self.hostpaths = hostpaths
-        self.vscode_workspace = Path(vscode_workspace_path).read_text()
-        self.container = container
-
-        self._stored.set_default(
-            debug_mode_started=False,
-            debug_mode_vscode_command=None,
-            debug_mode_password=None,
-        )
-
-        self.framework.observe(self.charm.on.config_changed, self._on_config_changed)
-        self.framework.observe(self.charm.on[container.name].pebble_ready, self._on_config_changed)
-        self.framework.observe(self.charm.on.update_status, self._on_update_status)
-
-    def _on_config_changed(self, _) -> None:
-        """Handler for the config-changed event."""
-        if not self.charm.unit.is_leader():
-            return
-
-        debug_mode_enabled = self.charm.config.get("debug-mode", False)
-        action = self.enable if debug_mode_enabled else self.disable
-        action()
-
-    def _on_update_status(self, _) -> None:
-        """Handler for the update-status event."""
-        if not self.charm.unit.is_leader() or not self.started:
-            return
-
-        self.charm.unit.status = ActiveStatus("debug-mode: ready")
-
-    @property
-    def started(self) -> bool:
-        """Indicates whether the debug-mode has started or not."""
-        return self._stored.debug_mode_started
-
-    @property
-    def command(self) -> str:
-        """Command to launch vscode."""
-        return self._stored.debug_mode_vscode_command
-
-    @property
-    def password(self) -> str:
-        """SSH password."""
-        return self._stored.debug_mode_password
-
-    def enable(self, service_name: str = None) -> None:
-        """Enable debug-mode.
-
-        This function mounts hostpaths of the OSM modules (if set), and
-        configures the container so it can be easily debugged. The setup
-        includes the configuration of SSH, environment variables, and
-        VSCode workspace and plugins.
-
-        Args:
-            service_name (str, optional): Pebble service name which has the desired environment
-                variables. Mandatory if there is more than one Pebble service configured.
-        """
-        hostpaths_to_reconfigure = self._hostpaths_to_reconfigure()
-        if self.started and not hostpaths_to_reconfigure:
-            self.charm.unit.status = ActiveStatus("debug-mode: ready")
-            return
-
-        logger.debug("enabling debug-mode")
-
-        # Mount hostpaths if set.
-        # If hostpaths are mounted, the statefulset will be restarted,
-        # and for that reason we return immediately. On restart, the hostpaths
-        # won't be mounted and then we can continue and setup the debug-mode.
-        if hostpaths_to_reconfigure:
-            self.charm.unit.status = MaintenanceStatus("debug-mode: configuring hostpaths")
-            self._configure_hostpaths(hostpaths_to_reconfigure)
-            return
-
-        self.charm.unit.status = MaintenanceStatus("debug-mode: starting")
-        password = secrets.token_hex(8)
-        self._setup_debug_mode(
-            password,
-            service_name,
-            mounted_hostpaths=[hp for hp in self.hostpaths if self.charm.config.get(hp.config)],
-        )
-
-        self._stored.debug_mode_vscode_command = self._get_vscode_command(get_pod_ip())
-        self._stored.debug_mode_password = password
-        self._stored.debug_mode_started = True
-        logger.info("debug-mode is ready")
-        self.charm.unit.status = ActiveStatus("debug-mode: ready")
-
-    def disable(self) -> None:
-        """Disable debug-mode."""
-        logger.debug("disabling debug-mode")
-        current_status = self.charm.unit.status
-        hostpaths_unmounted = self._unmount_hostpaths()
-
-        if not self._stored.debug_mode_started:
-            return
-        self._stored.debug_mode_started = False
-        self._stored.debug_mode_vscode_command = None
-        self._stored.debug_mode_password = None
-
-        if not hostpaths_unmounted:
-            self.charm.unit.status = current_status
-            self._restart()
-
-    def _hostpaths_to_reconfigure(self) -> List[HostPath]:
-        hostpaths_to_reconfigure: List[HostPath] = []
-        client = Client()
-        statefulset = client.get(StatefulSet, self.charm.app.name, namespace=self.charm.model.name)
-        volumes = statefulset.spec.template.spec.volumes
-
-        for hostpath in self.hostpaths:
-            hostpath_is_set = True if self.charm.config.get(hostpath.config) else False
-            hostpath_already_configured = next(
-                (True for volume in volumes if volume.name == hostpath.config), False
-            )
-            if hostpath_is_set != hostpath_already_configured:
-                hostpaths_to_reconfigure.append(hostpath)
-
-        return hostpaths_to_reconfigure
-
-    def _setup_debug_mode(
-        self,
-        password: str,
-        service_name: str = None,
-        mounted_hostpaths: List[HostPath] = [],
-    ) -> None:
-        services = self.container.get_plan().services
-        if not service_name and len(services) != 1:
-            raise Exception("Cannot start debug-mode: please set the service_name")
-
-        service = None
-        if not service_name:
-            service_name, service = services.popitem()
-        if not service:
-            service = services.get(service_name)
-
-        logger.debug(f"getting environment variables from service {service_name}")
-        environment = service.environment
-        environment_file_content = "\n".join(
-            [f'export {key}="{value}"' for key, value in environment.items()]
-        )
-        logger.debug(f"pushing environment file to {self.container.name} container")
-        self.container.push("/debug.envs", environment_file_content)
-
-        # Push VSCode workspace
-        logger.debug(f"pushing vscode workspace to {self.container.name} container")
-        self.container.push("/debug.code-workspace", self.vscode_workspace)
-
-        # Execute debugging script
-        logger.debug(f"pushing debug-mode setup script to {self.container.name} container")
-        self.container.push("/debug.sh", _DEBUG_SCRIPT.format(password), permissions=0o777)
-        logger.debug(f"executing debug-mode setup script in {self.container.name} container")
-        self.container.exec(["/debug.sh"]).wait_output()
-        logger.debug(f"stopping service {service_name} in {self.container.name} container")
-        self.container.stop(service_name)
-
-        # Add symlinks to mounted hostpaths
-        for hostpath in mounted_hostpaths:
-            logger.debug(f"adding symlink for {hostpath.config}")
-            if len(hostpath.sub_module_dict) > 0:
-                for sub_module in hostpath.sub_module_dict.keys():
-                    self.container.exec(["rm", "-rf", hostpath.sub_module_dict[sub_module].container_path]).wait_output()
-                    self.container.exec(
-                        [
-                            "ln",
-                            "-s",
-                            hostpath.sub_module_dict[sub_module].sub_module_path,
-                            hostpath.sub_module_dict[sub_module].container_path,
-                        ]
-                    )
-
-            else:
-                self.container.exec(["rm", "-rf", hostpath.container_path]).wait_output()
-                self.container.exec(
-                    [
-                        "ln",
-                        "-s",
-                        f"{hostpath.mount_path}/{hostpath.module_name}",
-                        hostpath.container_path,
-                    ]
-                )
-
-    def _configure_hostpaths(self, hostpaths: List[HostPath]):
-        client = Client()
-        statefulset = client.get(StatefulSet, self.charm.app.name, namespace=self.charm.model.name)
-
-        for hostpath in hostpaths:
-            if self.charm.config.get(hostpath.config):
-                self._add_hostpath_to_statefulset(hostpath, statefulset)
-            else:
-                self._delete_hostpath_from_statefulset(hostpath, statefulset)
-
-        client.replace(statefulset)
-
-    def _unmount_hostpaths(self) -> bool:
-        client = Client()
-        hostpath_unmounted = False
-        statefulset = client.get(StatefulSet, self.charm.app.name, namespace=self.charm.model.name)
-
-        for hostpath in self.hostpaths:
-            if self._delete_hostpath_from_statefulset(hostpath, statefulset):
-                hostpath_unmounted = True
-
-        if hostpath_unmounted:
-            client.replace(statefulset)
-
-        return hostpath_unmounted
-
-    def _add_hostpath_to_statefulset(self, hostpath: HostPath, statefulset: StatefulSet):
-        # Add volume
-        logger.debug(f"adding volume {hostpath.config} to {self.charm.app.name} statefulset")
-        volume = Volume(
-            hostpath.config,
-            hostPath=HostPathVolumeSource(
-                path=self.charm.config[hostpath.config],
-                type="Directory",
-            ),
-        )
-        statefulset.spec.template.spec.volumes.append(volume)
-
-        # Add volumeMount
-        for statefulset_container in statefulset.spec.template.spec.containers:
-            if statefulset_container.name != self.container.name:
-                continue
-
-            logger.debug(
-                f"adding volumeMount {hostpath.config} to {self.container.name} container"
-            )
-            statefulset_container.volumeMounts.append(
-                VolumeMount(mountPath=hostpath.mount_path, name=hostpath.config)
-            )
-
-    def _delete_hostpath_from_statefulset(self, hostpath: HostPath, statefulset: StatefulSet):
-        hostpath_unmounted = False
-        for volume in statefulset.spec.template.spec.volumes:
-
-            if hostpath.config != volume.name:
-                continue
-
-            # Remove volumeMount
-            for statefulset_container in statefulset.spec.template.spec.containers:
-                if statefulset_container.name != self.container.name:
-                    continue
-                for volume_mount in statefulset_container.volumeMounts:
-                    if volume_mount.name != hostpath.config:
-                        continue
-
-                    logger.debug(
-                        f"removing volumeMount {hostpath.config} from {self.container.name} container"
-                    )
-                    statefulset_container.volumeMounts.remove(volume_mount)
-
-            # Remove volume
-            logger.debug(
-                f"removing volume {hostpath.config} from {self.charm.app.name} statefulset"
-            )
-            statefulset.spec.template.spec.volumes.remove(volume)
-
-            hostpath_unmounted = True
-        return hostpath_unmounted
-
-    def _get_vscode_command(
-        self,
-        pod_ip: str,
-        user: str = "root",
-        workspace_path: str = "/debug.code-workspace",
-    ) -> str:
-        return f"code --remote ssh-remote+{user}@{pod_ip} {workspace_path}"
-
-    def _restart(self):
-        self.container.exec(["kill", "-HUP", "1"])
diff --git a/installers/charm/osm-lcm/lib/charms/osm_ro/v0/ro.py b/installers/charm/osm-lcm/lib/charms/osm_ro/v0/ro.py
deleted file mode 100644 (file)
index 79bee5e..0000000
+++ /dev/null
@@ -1,178 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# Learn more at: https://juju.is/docs/sdk
-
-"""Ro library.
-
-This [library](https://juju.is/docs/sdk/libraries) implements both sides of the
-`ro` [interface](https://juju.is/docs/sdk/relations).
-
-The *provider* side of this interface is implemented by the
-[osm-ro Charmed Operator](https://charmhub.io/osm-ro).
-
-Any Charmed Operator that *requires* RO for providing its
-service should implement the *requirer* side of this interface.
-
-In a nutshell using this library to implement a Charmed Operator *requiring*
-RO would look like
-
-```
-$ charmcraft fetch-lib charms.osm_ro.v0.ro
-```
-
-`metadata.yaml`:
-
-```
-requires:
-  ro:
-    interface: ro
-    limit: 1
-```
-
-`src/charm.py`:
-
-```
-from charms.osm_ro.v0.ro import RoRequires
-from ops.charm import CharmBase
-
-
-class MyCharm(CharmBase):
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.ro = RoRequires(self)
-        self.framework.observe(
-            self.on["ro"].relation_changed,
-            self._on_ro_relation_changed,
-        )
-        self.framework.observe(
-            self.on["ro"].relation_broken,
-            self._on_ro_relation_broken,
-        )
-        self.framework.observe(
-            self.on["ro"].relation_broken,
-            self._on_ro_broken,
-        )
-
-    def _on_ro_relation_broken(self, event):
-        # Get RO host and port
-        host: str = self.ro.host
-        port: int = self.ro.port
-        # host => "osm-ro"
-        # port => 9999
-
-    def _on_ro_broken(self, event):
-        # Stop service
-        # ...
-        self.unit.status = BlockedStatus("need ro relation")
-```
-
-You can file bugs
-[here](https://osm.etsi.org/bugzilla/enter_bug.cgi), selecting the `devops` module!
-"""
-from typing import Optional
-
-from ops.charm import CharmBase, CharmEvents
-from ops.framework import EventBase, EventSource, Object
-from ops.model import Relation
-
-
-# The unique Charmhub library identifier, never change it
-LIBID = "a34c3331a43f4f6db2b1499ff4d1390d"
-
-# 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 = 1
-
-RO_HOST_APP_KEY = "host"
-RO_PORT_APP_KEY = "port"
-
-
-class RoRequires(Object):  # pragma: no cover
-    """Requires-side of the Ro relation."""
-
-    def __init__(self, charm: CharmBase, endpoint_name: str = "ro") -> None:
-        super().__init__(charm, endpoint_name)
-        self.charm = charm
-        self._endpoint_name = endpoint_name
-
-    @property
-    def host(self) -> str:
-        """Get ro hostname."""
-        relation: Relation = self.model.get_relation(self._endpoint_name)
-        return (
-            relation.data[relation.app].get(RO_HOST_APP_KEY)
-            if relation and relation.app
-            else None
-        )
-
-    @property
-    def port(self) -> int:
-        """Get ro port number."""
-        relation: Relation = self.model.get_relation(self._endpoint_name)
-        return (
-            int(relation.data[relation.app].get(RO_PORT_APP_KEY))
-            if relation and relation.app
-            else None
-        )
-
-
-class RoProvides(Object):
-    """Provides-side of the Ro relation."""
-
-    def __init__(self, charm: CharmBase, endpoint_name: str = "ro") -> None:
-        super().__init__(charm, endpoint_name)
-        self._endpoint_name = endpoint_name
-
-    def set_host_info(self, host: str, port: int, relation: Optional[Relation] = None) -> None:
-        """Set Ro host and port.
-
-        This function writes in the application data of the relation, therefore,
-        only the unit leader can call it.
-
-        Args:
-            host (str): Ro hostname or IP address.
-            port (int): Ro port.
-            relation (Optional[Relation]): Relation to update.
-                                           If not specified, all relations will be updated.
-
-        Raises:
-            Exception: if a non-leader unit calls this function.
-        """
-        if not self.model.unit.is_leader():
-            raise Exception("only the leader set host information.")
-
-        if relation:
-            self._update_relation_data(host, port, relation)
-            return
-
-        for relation in self.model.relations[self._endpoint_name]:
-            self._update_relation_data(host, port, relation)
-
-    def _update_relation_data(self, host: str, port: int, relation: Relation) -> None:
-        """Update data in relation if needed."""
-        relation.data[self.model.app][RO_HOST_APP_KEY] = host
-        relation.data[self.model.app][RO_PORT_APP_KEY] = str(port)
diff --git a/installers/charm/osm-lcm/lib/charms/osm_vca_integrator/v0/vca.py b/installers/charm/osm-lcm/lib/charms/osm_vca_integrator/v0/vca.py
deleted file mode 100644 (file)
index 21dac69..0000000
+++ /dev/null
@@ -1,221 +0,0 @@
-# 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/osm-lcm/metadata.yaml b/installers/charm/osm-lcm/metadata.yaml
deleted file mode 100644 (file)
index b7dfa3d..0000000
+++ /dev/null
@@ -1,66 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Overview on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
-
-name: osm-lcm
-
-# The following metadata are human-readable and will be published prominently on Charmhub.
-
-display-name: OSM LCM
-
-summary: OSM Lifecycle Management (LCM)
-
-description: |
-  A Kubernetes operator that deploys the OSM's Lifecycle Management (LCM).
-
-  osm-lcm is the Lightweight Build Life Cycle Management for OSM.
-  It interact with RO module for resource orchestration and N2VC for VNF configuration.
-
-  This charm doesn't make sense on its own.
-  See more:
-    - https://charmhub.io/osm
-
-containers:
-  lcm:
-    resource: lcm-image
-
-# This file populates the Resources tab on Charmhub.
-
-resources:
-  lcm-image:
-    type: oci-image
-    description: OCI image for lcm
-    upstream-source: opensourcemano/lcm
-
-requires:
-  kafka:
-    interface: kafka
-    limit: 1
-  mongodb:
-    interface: mongodb_client
-    limit: 1
-  ro:
-    interface: ro
-    limit: 1
-  vca:
-    interface: osm-vca
diff --git a/installers/charm/osm-lcm/pyproject.toml b/installers/charm/osm-lcm/pyproject.toml
deleted file mode 100644 (file)
index 16cf0f4..0000000
+++ /dev/null
@@ -1,52 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-
-# 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", "E501", "D107"]
-# D100, D101, D102, D103: Ignore missing docstrings in tests
-per-file-ignores = ["tests/*:D100,D101,D102,D103,D104"]
-docstring-convention = "google"
diff --git a/installers/charm/osm-lcm/requirements.txt b/installers/charm/osm-lcm/requirements.txt
deleted file mode 100644 (file)
index 398d4ad..0000000
+++ /dev/null
@@ -1,23 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-ops < 2.2
-lightkube
-lightkube-models
-# git+https://github.com/charmed-osm/config-validator/
diff --git a/installers/charm/osm-lcm/src/charm.py b/installers/charm/osm-lcm/src/charm.py
deleted file mode 100755 (executable)
index 2ea9086..0000000
+++ /dev/null
@@ -1,290 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# Learn more at: https://juju.is/docs/sdk
-
-"""OSM LCM charm.
-
-See more: https://charmhub.io/osm
-"""
-
-import logging
-from typing import Any, Dict
-
-from charms.data_platform_libs.v0.data_interfaces import DatabaseRequires
-from charms.kafka_k8s.v0.kafka import KafkaRequires, _KafkaAvailableEvent
-from charms.osm_libs.v0.utils import (
-    CharmError,
-    DebugMode,
-    HostPath,
-    check_container_ready,
-    check_service_active,
-)
-from charms.osm_ro.v0.ro import RoRequires
-from charms.osm_vca_integrator.v0.vca import VcaDataChangedEvent, VcaRequires
-from ops.charm import ActionEvent, CharmBase, CharmEvents
-from ops.framework import EventSource, StoredState
-from ops.main import main
-from ops.model import ActiveStatus, Container
-
-HOSTPATHS = [
-    HostPath(
-        config="lcm-hostpath",
-        container_path="/usr/lib/python3/dist-packages/osm_lcm",
-    ),
-    HostPath(
-        config="common-hostpath",
-        container_path="/usr/lib/python3/dist-packages/osm_common",
-    ),
-    HostPath(
-        config="n2vc-hostpath",
-        container_path="/usr/lib/python3/dist-packages/n2vc",
-    ),
-]
-
-logger = logging.getLogger(__name__)
-
-
-class LcmEvents(CharmEvents):
-    """LCM events."""
-
-    vca_data_changed = EventSource(VcaDataChangedEvent)
-    kafka_available = EventSource(_KafkaAvailableEvent)
-
-
-class OsmLcmCharm(CharmBase):
-    """OSM LCM Kubernetes sidecar charm."""
-
-    container_name = "lcm"
-    service_name = "lcm"
-    on = LcmEvents()
-    _stored = StoredState()
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.vca = VcaRequires(self)
-        self.kafka = KafkaRequires(self)
-        self.mongodb_client = DatabaseRequires(
-            self, "mongodb", database_name="osm", extra_user_roles="admin"
-        )
-        self._observe_charm_events()
-        self.ro = RoRequires(self)
-        self.container: Container = self.unit.get_container(self.container_name)
-        self.debug_mode = DebugMode(self, self._stored, self.container, HOSTPATHS)
-
-    # ---------------------------------------------------------------------------
-    #   Handlers for Charm Events
-    # ---------------------------------------------------------------------------
-
-    def _on_config_changed(self, _) -> None:
-        """Handler for the config-changed event."""
-        try:
-            self._validate_config()
-            self._check_relations()
-            # Check if the container is ready.
-            # Eventually it will become ready after the first pebble-ready event.
-            check_container_ready(self.container)
-            if not self.debug_mode.started:
-                self._configure_service(self.container)
-
-            # Update charm status
-            self._on_update_status()
-        except CharmError as e:
-            logger.debug(e.message)
-            self.unit.status = e.status
-
-    def _on_update_status(self, _=None) -> None:
-        """Handler for the update-status event."""
-        try:
-            self._validate_config()
-            self._check_relations()
-            check_container_ready(self.container)
-            if self.debug_mode.started:
-                return
-            check_service_active(self.container, self.service_name)
-            self.unit.status = ActiveStatus()
-        except CharmError as e:
-            logger.debug(e.message)
-            self.unit.status = e.status
-
-    def _on_required_relation_broken(self, _) -> None:
-        """Handler for required relation-broken events."""
-        try:
-            check_container_ready(self.container)
-            check_service_active(self.container, self.service_name)
-            self.container.stop(self.container_name)
-        except CharmError:
-            pass
-        self._on_update_status()
-
-    def _on_get_debug_mode_information_action(self, event: ActionEvent) -> None:
-        """Handler for the get-debug-mode-information action event."""
-        if not self.debug_mode.started:
-            event.fail(
-                f"debug-mode has not started. Hint: juju config {self.app.name} debug-mode=true"
-            )
-            return
-
-        debug_info = {"command": self.debug_mode.command, "password": self.debug_mode.password}
-        event.set_results(debug_info)
-
-    # ---------------------------------------------------------------------------
-    #   Validation, configuration and more
-    # ---------------------------------------------------------------------------
-
-    def _validate_config(self) -> None:
-        """Validate charm configuration.
-
-        Raises:
-            CharmError: if charm configuration is invalid.
-        """
-        logger.debug("validating charm config")
-        if self.config["log-level"].upper() not in [
-            "TRACE",
-            "DEBUG",
-            "INFO",
-            "WARN",
-            "ERROR",
-            "FATAL",
-        ]:
-            raise CharmError("invalid value for log-level option")
-
-    def _observe_charm_events(self) -> None:
-        event_handler_mapping = {
-            # Core lifecycle events
-            self.on.lcm_pebble_ready: self._on_config_changed,
-            self.on.config_changed: self._on_config_changed,
-            self.on.update_status: self._on_update_status,
-            # Relation events
-            self.on.kafka_available: self._on_config_changed,
-            self.on["kafka"].relation_broken: self._on_required_relation_broken,
-            self.mongodb_client.on.database_created: self._on_config_changed,
-            self.on["mongodb"].relation_broken: self._on_required_relation_broken,
-            self.on["ro"].relation_changed: self._on_config_changed,
-            self.on["ro"].relation_broken: self._on_required_relation_broken,
-            self.on.vca_data_changed: self._on_config_changed,
-            self.on["vca"].relation_broken: self._on_config_changed,
-            # Action events
-            self.on.get_debug_mode_information_action: self._on_get_debug_mode_information_action,
-        }
-        for event, handler in event_handler_mapping.items():
-            self.framework.observe(event, handler)
-
-    def _check_relations(self) -> None:
-        """Validate charm relations.
-
-        Raises:
-            CharmError: if charm configuration is invalid.
-        """
-        logger.debug("check for missing relations")
-        missing_relations = []
-
-        if not self.kafka.host or not self.kafka.port:
-            missing_relations.append("kafka")
-        if not self._is_database_available():
-            missing_relations.append("mongodb")
-        if not self.ro.host or not self.ro.port:
-            missing_relations.append("ro")
-
-        if missing_relations:
-            relations_str = ", ".join(missing_relations)
-            one_relation_missing = len(missing_relations) == 1
-            error_msg = f'need {relations_str} relation{"" if one_relation_missing else "s"}'
-            logger.warning(error_msg)
-            raise CharmError(error_msg)
-
-    def _is_database_available(self) -> bool:
-        try:
-            return self.mongodb_client.is_resource_created()
-        except KeyError:
-            return False
-
-    def _configure_service(self, container: Container) -> None:
-        """Add Pebble layer with the lcm service."""
-        logger.debug(f"configuring {self.app.name} service")
-        container.add_layer("lcm", self._get_layer(), combine=True)
-        container.replan()
-
-    def _get_layer(self) -> Dict[str, Any]:
-        """Get layer for Pebble."""
-        environments = {
-            # General configuration
-            "OSMLCM_GLOBAL_LOGLEVEL": self.config["log-level"].upper(),
-            # Kafka configuration
-            "OSMLCM_MESSAGE_DRIVER": "kafka",
-            "OSMLCM_MESSAGE_HOST": self.kafka.host,
-            "OSMLCM_MESSAGE_PORT": self.kafka.port,
-            # RO configuration
-            "OSMLCM_RO_HOST": self.ro.host,
-            "OSMLCM_RO_PORT": self.ro.port,
-            "OSMLCM_RO_TENANT": "osm",
-            # Database configuration
-            "OSMLCM_DATABASE_DRIVER": "mongo",
-            "OSMLCM_DATABASE_URI": self._get_mongodb_uri(),
-            "OSMLCM_DATABASE_COMMONKEY": self.config["database-commonkey"],
-            # Storage configuration
-            "OSMLCM_STORAGE_DRIVER": "mongo",
-            "OSMLCM_STORAGE_PATH": "/app/storage",
-            "OSMLCM_STORAGE_COLLECTION": "files",
-            "OSMLCM_STORAGE_URI": self._get_mongodb_uri(),
-            "OSMLCM_VCA_HELM_CA_CERTS": self.config["helm-ca-certs"],
-            "OSMLCM_VCA_STABLEREPOURL": self.config["helm-stable-repo-url"],
-        }
-        # Vca configuration
-        if self.vca.data:
-            environments["OSMLCM_VCA_ENDPOINTS"] = self.vca.data.endpoints
-            environments["OSMLCM_VCA_USER"] = self.vca.data.user
-            environments["OSMLCM_VCA_PUBKEY"] = self.vca.data.public_key
-            environments["OSMLCM_VCA_SECRET"] = self.vca.data.secret
-            environments["OSMLCM_VCA_CACERT"] = self.vca.data.cacert
-            if self.vca.data.lxd_cloud:
-                environments["OSMLCM_VCA_CLOUD"] = self.vca.data.lxd_cloud
-
-            if self.vca.data.k8s_cloud:
-                environments["OSMLCM_VCA_K8S_CLOUD"] = self.vca.data.k8s_cloud
-            for key, value in self.vca.data.model_configs.items():
-                env_name = f'OSMLCM_VCA_MODEL_CONFIG_{key.upper().replace("-","_")}'
-                environments[env_name] = value
-
-        layer_config = {
-            "summary": "lcm layer",
-            "description": "pebble config layer for nbi",
-            "services": {
-                self.service_name: {
-                    "override": "replace",
-                    "summary": "lcm service",
-                    "command": "python3 -m osm_lcm.lcm",
-                    "startup": "enabled",
-                    "user": "appuser",
-                    "group": "appuser",
-                    "environment": environments,
-                }
-            },
-        }
-        return layer_config
-
-    def _get_mongodb_uri(self):
-        return list(self.mongodb_client.fetch_relation_data().values())[0]["uris"]
-
-
-if __name__ == "__main__":  # pragma: no cover
-    main(OsmLcmCharm)
diff --git a/installers/charm/osm-lcm/src/legacy_interfaces.py b/installers/charm/osm-lcm/src/legacy_interfaces.py
deleted file mode 100644 (file)
index d56f31d..0000000
+++ /dev/null
@@ -1,107 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-# flake8: noqa
-
-import ops
-
-
-class BaseRelationClient(ops.framework.Object):
-    """Requires side of a Kafka Endpoint"""
-
-    def __init__(
-        self, charm: ops.charm.CharmBase, relation_name: str, mandatory_fields: list = []
-    ):
-        super().__init__(charm, relation_name)
-        self.relation_name = relation_name
-        self.mandatory_fields = mandatory_fields
-        self._update_relation()
-
-    def get_data_from_unit(self, key: str):
-        if not self.relation:
-            # This update relation doesn't seem to be needed, but I added it because apparently
-            # the data is empty in the unit tests.
-            # In reality, the constructor is called in every hook.
-            # In the unit tests when doing an update_relation_data, apparently it is not called.
-            self._update_relation()
-        if self.relation:
-            for unit in self.relation.units:
-                data = self.relation.data[unit].get(key)
-                if data:
-                    return data
-
-    def get_data_from_app(self, key: str):
-        if not self.relation or self.relation.app not in self.relation.data:
-            # This update relation doesn't seem to be needed, but I added it because apparently
-            # the data is empty in the unit tests.
-            # In reality, the constructor is called in every hook.
-            # In the unit tests when doing an update_relation_data, apparently it is not called.
-            self._update_relation()
-        if self.relation and self.relation.app in self.relation.data:
-            data = self.relation.data[self.relation.app].get(key)
-            if data:
-                return data
-
-    def is_missing_data_in_unit(self):
-        return not all([self.get_data_from_unit(field) for field in self.mandatory_fields])
-
-    def is_missing_data_in_app(self):
-        return not all([self.get_data_from_app(field) for field in self.mandatory_fields])
-
-    def _update_relation(self):
-        self.relation = self.framework.model.get_relation(self.relation_name)
-
-
-class MongoClient(BaseRelationClient):
-    """Requires side of a Mongo Endpoint"""
-
-    mandatory_fields_mapping = {
-        "reactive": ["connection_string"],
-        "ops": ["replica_set_uri", "replica_set_name"],
-    }
-
-    def __init__(self, charm: ops.charm.CharmBase, relation_name: str):
-        super().__init__(charm, relation_name, mandatory_fields=[])
-
-    @property
-    def connection_string(self):
-        if self.is_opts():
-            replica_set_uri = self.get_data_from_unit("replica_set_uri")
-            replica_set_name = self.get_data_from_unit("replica_set_name")
-            return f"{replica_set_uri}?replicaSet={replica_set_name}"
-        else:
-            return self.get_data_from_unit("connection_string")
-
-    def is_opts(self):
-        return not self.is_missing_data_in_unit_ops()
-
-    def is_missing_data_in_unit(self):
-        return self.is_missing_data_in_unit_ops() and self.is_missing_data_in_unit_reactive()
-
-    def is_missing_data_in_unit_ops(self):
-        return not all(
-            [self.get_data_from_unit(field) for field in self.mandatory_fields_mapping["ops"]]
-        )
-
-    def is_missing_data_in_unit_reactive(self):
-        return not all(
-            [self.get_data_from_unit(field) for field in self.mandatory_fields_mapping["reactive"]]
-        )
diff --git a/installers/charm/osm-lcm/tests/integration/test_charm.py b/installers/charm/osm-lcm/tests/integration/test_charm.py
deleted file mode 100644 (file)
index 00bb260..0000000
+++ /dev/null
@@ -1,218 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-# Learn more about testing at: https://juju.is/docs/sdk/testing
-
-import asyncio
-import logging
-import shlex
-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())
-LCM_APP = METADATA["name"]
-KAFKA_CHARM = "kafka-k8s"
-KAFKA_APP = "kafka"
-MONGO_DB_CHARM = "mongodb-k8s"
-MONGO_DB_APP = "mongodb"
-RO_CHARM = "osm-ro"
-RO_APP = "ro"
-ZOOKEEPER_CHARM = "zookeeper-k8s"
-ZOOKEEPER_APP = "zookeeper"
-VCA_CHARM = "osm-vca-integrator"
-VCA_APP = "vca"
-APPS = [KAFKA_APP, MONGO_DB_APP, ZOOKEEPER_APP, RO_APP, LCM_APP]
-
-
-@pytest.mark.abort_on_fail
-async def test_lcm_is_deployed(ops_test: OpsTest):
-    charm = await ops_test.build_charm(".")
-    resources = {"lcm-image": METADATA["resources"]["lcm-image"]["upstream-source"]}
-    ro_deploy_cmd = f"juju deploy {RO_CHARM} {RO_APP} --resource ro-image=opensourcemano/ro:testing-daily --channel=latest/beta --series=jammy"
-
-    await asyncio.gather(
-        ops_test.model.deploy(
-            charm, resources=resources, application_name=LCM_APP, series="jammy"
-        ),
-        # RO charm has to be deployed differently since
-        # bug https://github.com/juju/python-libjuju/issues/822
-        # deploys different charms wrt cli
-        ops_test.run(*shlex.split(ro_deploy_cmd), check=True),
-        ops_test.model.deploy(KAFKA_CHARM, application_name=KAFKA_APP, channel="stable"),
-        ops_test.model.deploy(MONGO_DB_CHARM, application_name=MONGO_DB_APP, channel="5/edge"),
-        ops_test.model.deploy(ZOOKEEPER_CHARM, application_name=ZOOKEEPER_APP, channel="stable"),
-    )
-
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS,
-            timeout=300,
-        )
-    assert ops_test.model.applications[LCM_APP].status == "blocked"
-    unit = ops_test.model.applications[LCM_APP].units[0]
-    assert unit.workload_status_message == "need kafka, mongodb, ro relations"
-
-    logger.info("Adding relations for other components")
-    await ops_test.model.add_relation(KAFKA_APP, ZOOKEEPER_APP)
-    await ops_test.model.add_relation(
-        "{}:mongodb".format(RO_APP), "{}:database".format(MONGO_DB_APP)
-    )
-    await ops_test.model.add_relation(RO_APP, KAFKA_APP)
-
-    logger.info("Adding relations for LCM")
-    await ops_test.model.add_relation(
-        "{}:mongodb".format(LCM_APP), "{}:database".format(MONGO_DB_APP)
-    )
-    await ops_test.model.add_relation(LCM_APP, KAFKA_APP)
-    await ops_test.model.add_relation(LCM_APP, RO_APP)
-
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS,
-            status="active",
-            timeout=300,
-        )
-
-
-@pytest.mark.abort_on_fail
-async def test_lcm_scales_up(ops_test: OpsTest):
-    logger.info("Scaling up osm-lcm")
-    expected_units = 3
-    assert len(ops_test.model.applications[LCM_APP].units) == 1
-    await ops_test.model.applications[LCM_APP].scale(expected_units)
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=[LCM_APP], status="active", timeout=1000, wait_for_exact_units=expected_units
-        )
-
-
-@pytest.mark.abort_on_fail
-@pytest.mark.parametrize("relation_to_remove", [RO_APP, KAFKA_APP, MONGO_DB_APP])
-async def test_lcm_blocks_without_relation(ops_test: OpsTest, relation_to_remove):
-    logger.info("Removing relation: %s", relation_to_remove)
-    # mongoDB relation is named "database"
-    local_relation = relation_to_remove
-    if relation_to_remove == MONGO_DB_APP:
-        local_relation = "database"
-    await asyncio.gather(
-        ops_test.model.applications[relation_to_remove].remove_relation(local_relation, LCM_APP)
-    )
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(apps=[LCM_APP])
-    assert ops_test.model.applications[LCM_APP].status == "blocked"
-    for unit in ops_test.model.applications[LCM_APP].units:
-        assert unit.workload_status_message == f"need {relation_to_remove} relation"
-    await ops_test.model.add_relation(LCM_APP, relation_to_remove)
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS,
-            status="active",
-            timeout=300,
-        )
-
-
-@pytest.mark.abort_on_fail
-async def test_lcm_action_debug_mode_disabled(ops_test: OpsTest):
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS,
-            status="active",
-            timeout=300,
-        )
-    logger.info("Running action 'get-debug-mode-information'")
-    action = (
-        await ops_test.model.applications[LCM_APP]
-        .units[0]
-        .run_action("get-debug-mode-information")
-    )
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(apps=[LCM_APP])
-    status = await ops_test.model.get_action_status(uuid_or_prefix=action.entity_id)
-    assert status[action.entity_id] == "failed"
-
-
-@pytest.mark.abort_on_fail
-async def test_lcm_action_debug_mode_enabled(ops_test: OpsTest):
-    await ops_test.model.applications[LCM_APP].set_config({"debug-mode": "true"})
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS,
-            status="active",
-            timeout=1000,
-        )
-    logger.info("Running action 'get-debug-mode-information'")
-    # list of units is not ordered
-    unit_id = list(
-        filter(
-            lambda x: (x.entity_id == f"{LCM_APP}/0"), ops_test.model.applications[LCM_APP].units
-        )
-    )[0]
-    action = await unit_id.run_action("get-debug-mode-information")
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(apps=[LCM_APP])
-    status = await ops_test.model.get_action_status(uuid_or_prefix=action.entity_id)
-    message = await ops_test.model.get_action_output(action_uuid=action.entity_id)
-    assert status[action.entity_id] == "completed"
-    assert "command" in message
-    assert "password" in message
-
-
-@pytest.mark.abort_on_fail
-async def test_lcm_integration_vca(ops_test: OpsTest):
-    await asyncio.gather(
-        ops_test.model.deploy(
-            VCA_CHARM, application_name=VCA_APP, channel="latest/beta", series="jammy"
-        ),
-    )
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=[VCA_APP],
-            timeout=300,
-        )
-    controllers = (Path.home() / ".local/share/juju/controllers.yaml").read_text()
-    accounts = (Path.home() / ".local/share/juju/accounts.yaml").read_text()
-    public_key = (Path.home() / ".local/share/juju/ssh/juju_id_rsa.pub").read_text()
-    await ops_test.model.applications[VCA_APP].set_config(
-        {
-            "controllers": controllers,
-            "accounts": accounts,
-            "public-key": public_key,
-            "k8s-cloud": "microk8s",
-        }
-    )
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS + [VCA_APP],
-            status="active",
-            timeout=1000,
-        )
-    await ops_test.model.add_relation(LCM_APP, VCA_APP)
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS + [VCA_APP],
-            status="active",
-            timeout=300,
-        )
diff --git a/installers/charm/osm-lcm/tests/unit/test_charm.py b/installers/charm/osm-lcm/tests/unit/test_charm.py
deleted file mode 100644 (file)
index 41cfb00..0000000
+++ /dev/null
@@ -1,88 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-# Learn more about testing at: https://juju.is/docs/sdk/testing
-
-import pytest
-from ops.model import ActiveStatus, BlockedStatus
-from ops.testing import Harness
-from pytest_mock import MockerFixture
-
-from charm import CharmError, OsmLcmCharm, check_service_active
-
-container_name = "lcm"
-service_name = "lcm"
-
-
-@pytest.fixture
-def harness(mocker: MockerFixture):
-    harness = Harness(OsmLcmCharm)
-    harness.begin()
-    harness.container_pebble_ready(container_name)
-    yield harness
-    harness.cleanup()
-
-
-def test_missing_relations(harness: Harness):
-    harness.charm.on.config_changed.emit()
-    assert type(harness.charm.unit.status) == BlockedStatus
-    assert all(
-        relation in harness.charm.unit.status.message for relation in ["mongodb", "kafka", "ro"]
-    )
-
-
-def test_ready(harness: Harness):
-    _add_relations(harness)
-    assert harness.charm.unit.status == ActiveStatus()
-
-
-def test_container_stops_after_relation_broken(harness: Harness):
-    harness.charm.on[container_name].pebble_ready.emit(container_name)
-    container = harness.charm.unit.get_container(container_name)
-    relation_ids = _add_relations(harness)
-    check_service_active(container, service_name)
-    harness.remove_relation(relation_ids[0])
-    with pytest.raises(CharmError):
-        check_service_active(container, service_name)
-
-
-def _add_relations(harness: Harness):
-    relation_ids = []
-    # Add mongo relation
-    relation_id = harness.add_relation("mongodb", "mongodb")
-    harness.add_relation_unit(relation_id, "mongodb/0")
-    harness.update_relation_data(
-        relation_id,
-        "mongodb",
-        {"uris": "mongodb://:1234", "username": "user", "password": "password"},
-    )
-    relation_ids.append(relation_id)
-    # Add kafka relation
-    relation_id = harness.add_relation("kafka", "kafka")
-    harness.add_relation_unit(relation_id, "kafka/0")
-    harness.update_relation_data(relation_id, "kafka", {"host": "kafka", "port": "9092"})
-    relation_ids.append(relation_id)
-    # Add ro relation
-    relation_id = harness.add_relation("ro", "ro")
-    harness.add_relation_unit(relation_id, "ro/0")
-    harness.update_relation_data(relation_id, "ro", {"host": "ro", "port": "9090"})
-    relation_ids.append(relation_id)
-    return relation_ids
diff --git a/installers/charm/osm-lcm/tox.ini b/installers/charm/osm-lcm/tox.ini
deleted file mode 100644 (file)
index 2d95eca..0000000
+++ /dev/null
@@ -1,92 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-
-[tox]
-skipsdist=True
-skip_missing_interpreters = True
-envlist = lint, unit, integration
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-all_path = {[vars]src_path} {[vars]tst_path} 
-
-[testenv]
-basepython = python3.8
-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-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    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
-    # 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} \
-        -m pytest --ignore={[vars]tst_path}integration -v --tb native -s {posargs}
-    coverage report
-    coverage xml
-
-[testenv:integration]
-description = Run integration tests
-deps =
-    pytest
-    juju<3
-    pytest-operator
-    -r{toxinidir}/requirements.txt
-commands =
-    pytest -v --tb native --ignore={[vars]tst_path}unit --log-cli-level=INFO -s {posargs} --cloud microk8s
diff --git a/installers/charm/osm-mon/.gitignore b/installers/charm/osm-mon/.gitignore
deleted file mode 100644 (file)
index 87d0a58..0000000
+++ /dev/null
@@ -1,29 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-venv/
-build/
-*.charm
-.tox/
-.coverage
-coverage.xml
-__pycache__/
-*.py[cod]
-.vscode
\ No newline at end of file
diff --git a/installers/charm/osm-mon/.jujuignore b/installers/charm/osm-mon/.jujuignore
deleted file mode 100644 (file)
index 17c7a8b..0000000
+++ /dev/null
@@ -1,23 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-/venv
-*.py[cod]
-*.charm
diff --git a/installers/charm/osm-mon/CONTRIBUTING.md b/installers/charm/osm-mon/CONTRIBUTING.md
deleted file mode 100644 (file)
index 1ade9b3..0000000
+++ /dev/null
@@ -1,78 +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.
-
-For those usages not covered by the Apache License, Version 2.0 please
-contact: legal@canonical.com
-
-To get in touch with the maintainers, please contact:
-osm-charmers@lists.launchpad.net -->
-
-# Contributing
-
-## Overview
-
-This documents explains the processes and practices recommended for contributing enhancements to
-this operator.
-
-- Generally, before developing enhancements to this charm, you should consider [opening an issue
-  ](https://osm.etsi.org/bugzilla/enter_bug.cgi?product=OSM) explaining your use case. (Component=devops, version=master)
-- If you would like to chat with us about your use-cases or proposed implementation, you can reach
-  us at [OSM Juju public channel](https://opensourcemano.slack.com/archives/C027KJGPECA).
-- 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 gerrit patch onto
-  the `master` branch.
-
-## 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 dev
-# Enable DEBUG logging
-juju model-config logging-config="<root>=INFO;unit=DEBUG"
-# Deploy the charm
-juju deploy ./osm-mon_ubuntu-22.04-amd64.charm \
-    --resource mon-image=opensourcemano/mon:testing-daily --series jammy
-```
diff --git a/installers/charm/osm-mon/LICENSE b/installers/charm/osm-mon/LICENSE
deleted file mode 100644 (file)
index 7e9d504..0000000
+++ /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 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.
diff --git a/installers/charm/osm-mon/README.md b/installers/charm/osm-mon/README.md
deleted file mode 100644 (file)
index 8d4eb22..0000000
+++ /dev/null
@@ -1,43 +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.
-
-For those usages not covered by the Apache License, Version 2.0 please
-contact: legal@canonical.com
-
-To get in touch with the maintainers, please contact:
-osm-charmers@lists.launchpad.net -->
-
-<!--
-Avoid using this README file for information that is maintained or published elsewhere, e.g.:
-
-* metadata.yaml > published on Charmhub
-* documentation > published on (or linked to from) Charmhub
-* detailed contribution guide > documentation or CONTRIBUTING.md
-
-Use links instead.
--->
-
-# OSM MON
-
-Charmhub package name: osm-mon
-More information: https://charmhub.io/osm-mon
-
-## Other resources
-
-* [Read more](https://osm.etsi.org/docs/user-guide/latest/)
-
-* [Contributing](https://osm.etsi.org/gitweb/?p=osm/devops.git;a=blob;f=installers/charm/osm-mon/CONTRIBUTING.md)
-
-* See the [Juju SDK documentation](https://juju.is/docs/sdk) for more information about developing and improving charms.
-
diff --git a/installers/charm/osm-mon/actions.yaml b/installers/charm/osm-mon/actions.yaml
deleted file mode 100644 (file)
index 0d73468..0000000
+++ /dev/null
@@ -1,26 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Actions tab on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
-
-get-debug-mode-information:
-  description: Get information to debug the container
diff --git a/installers/charm/osm-mon/charmcraft.yaml b/installers/charm/osm-mon/charmcraft.yaml
deleted file mode 100644 (file)
index f5e3ff3..0000000
+++ /dev/null
@@ -1,36 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-
-type: charm
-bases:
-  - build-on:
-      - name: "ubuntu"
-        channel: "22.04"
-    run-on:
-      - name: "ubuntu"
-        channel: "22.04"
-
-parts:
-  charm:
-    # build-packages:
-    #   - git
-    prime:
-      - files/*
diff --git a/installers/charm/osm-mon/config.yaml b/installers/charm/osm-mon/config.yaml
deleted file mode 100644 (file)
index cb2eb99..0000000
+++ /dev/null
@@ -1,140 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Configure tab on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
-
-options:
-  log-level:
-    default: "INFO"
-    description: |
-      Set the Logging Level.
-
-      Options:
-        - TRACE
-        - DEBUG
-        - INFO
-        - WARN
-        - ERROR
-        - FATAL
-    type: string
-  database-commonkey:
-    description: Database COMMON KEY
-    type: string
-    default: osm
-  openstack-default-granularity:
-    description: Openstack default granularity
-    type: int
-    default: 300
-  global-request-timeout:
-    description: Global request timeout
-    type: int
-    default: 10
-  collector-interval:
-    description: Collector interval
-    type: int
-    default: 30
-  evaluator-interval:
-    description: Evaluator interval
-    type: int
-    default: 30
-  grafana-url:
-    description: Grafana URL
-    type: string
-    default: http://grafana:3000
-  grafana-user:
-    description: Grafana user
-    type: string
-    default: admin
-  grafana-password:
-    description: Grafana password
-    type: string
-    default: admin
-  keystone-enabled:
-    description: MON will use Keystone backend
-    type: boolean
-    default: false
-  vm-infra-metrics:
-    description: Enables querying the VIMs asking for the status of the VMs
-    type: boolean
-    default: true
-  certificates:
-    type: string
-    description: |
-      comma-separated list of <name>:<content> certificates.
-      Where:
-        name: name of the file for the certificate
-        content: base64 content of the certificate
-      The path for the files is /certs.
-
-  # Debug-mode options
-  debug-mode:
-    type: boolean
-    description: |
-      Great for OSM Developers! (Not recommended for production deployments)
-
-      This action activates the Debug Mode, which sets up the container to be ready for debugging.
-      As part of the setup, SSH is enabled and a VSCode workspace file is automatically populated.
-
-      After enabling the debug-mode, execute the following command to get the information you need
-      to start debugging:
-        `juju run-action <unit name> get-debug-mode-information --wait`
-
-      The previous command returns the command you need to execute, and the SSH password that was set.
-
-      See also:
-        - https://charmhub.io/osm-mon/configure#mon-hostpath
-        - https://charmhub.io/osm-mon/configure#common-hostpath
-        - https://charmhub.io/osm-mon/configure#n2vc-hostpath
-    default: false
-  mon-hostpath:
-    type: string
-    description: |
-      Set this config to the local path of the MON module to persist the changes done during the
-      debug-mode session.
-
-      Example:
-        $ git clone "https://osm.etsi.org/gerrit/osm/MON" /home/ubuntu/MON
-        $ juju config mon mon-hostpath=/home/ubuntu/MON
-
-      This configuration only applies if option `debug-mode` is set to true.
-  common-hostpath:
-    type: string
-    description: |
-      Set this config to the local path of the common module to persist the changes done during the
-      debug-mode session.
-
-      Example:
-        $ git clone "https://osm.etsi.org/gerrit/osm/common" /home/ubuntu/common
-        $ juju config mon common-hostpath=/home/ubuntu/common
-
-      This configuration only applies if option `debug-mode` is set to true.
-  n2vc-hostpath:
-    type: string
-    description: |
-      Set this config to the local path of the N2VC module to persist the changes done during the
-      debug-mode session.
-
-      Example:
-        $ git clone "https://osm.etsi.org/gerrit/osm/N2VC" /home/ubuntu/N2VC
-        $ juju config mon n2vc-hostpath=/home/ubuntu/N2VC
-
-      This configuration only applies if option `debug-mode` is set to true.
diff --git a/installers/charm/osm-mon/files/vscode-workspace.json b/installers/charm/osm-mon/files/vscode-workspace.json
deleted file mode 100644 (file)
index 34c7718..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-{
-    "folders": [
-        {"path": "/usr/lib/python3/dist-packages/osm_mon"},
-        {"path": "/usr/lib/python3/dist-packages/osm_common"},
-        {"path": "/usr/lib/python3/dist-packages/n2vc"},
-    ],
-    "settings": {},
-    "launch": {
-        "version": "0.2.0",
-        "configurations": [
-            {
-                "name": "MON",
-                "type": "python",
-                "request": "launch",
-                "module": "osm_mon.nbi",
-                "justMyCode": false,
-            }
-
-            {
-                "name": "MON Server",
-                "type": "python",
-                "request": "launch",
-                "module": "osm_mon.cmd.mon_server",
-                "justMyCode": false,
-            },
-            {
-                "name": "MON evaluator",
-                "type": "python",
-                "request": "launch",
-                "module": "osm_mon.cmd.mon_evaluator",
-                "justMyCode": false,
-            },
-            {
-                "name": "MON collector",
-                "type": "python",
-                "request": "launch",
-                "module": "osm_mon.cmd.mon_collector",
-                "justMyCode": false,
-            },
-            {
-                "name": "MON dashboarder",
-                "type": "python",
-                "request": "launch",
-                "module": "osm_mon.cmd.mon_dashboarder",
-                "justMyCode": false,
-            },
-        ],
-    }
-}
\ No newline at end of file
diff --git a/installers/charm/osm-mon/lib/charms/data_platform_libs/v0/data_interfaces.py b/installers/charm/osm-mon/lib/charms/data_platform_libs/v0/data_interfaces.py
deleted file mode 100644 (file)
index b3da5aa..0000000
+++ /dev/null
@@ -1,1130 +0,0 @@
-# Copyright 2023 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.
-
-"""Library to manage the relation for the data-platform products.
-
-This library contains the Requires and Provides classes for handling the relation
-between an application and multiple managed application supported by the data-team:
-MySQL, Postgresql, MongoDB, Redis,  and Kakfa.
-
-### Database (MySQL, Postgresql, MongoDB, and Redis)
-
-#### Requires Charm
-This library is a uniform interface to a selection of common database
-metadata, with added custom events that add convenience to database management,
-and methods to consume the application related data.
-
-
-Following an example of using the DatabaseCreatedEvent, in the context of the
-application charm code:
-
-```python
-
-from charms.data_platform_libs.v0.data_interfaces import (
-    DatabaseCreatedEvent,
-    DatabaseRequires,
-)
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Charm events defined in the database requires charm library.
-        self.database = DatabaseRequires(self, relation_name="database", database_name="database")
-        self.framework.observe(self.database.on.database_created, self._on_database_created)
-
-    def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-
-        # Start application with rendered configuration
-        self._start_application(config_file)
-
-        # Set active status
-        self.unit.status = ActiveStatus("received database credentials")
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
--  database_created: event emitted when the requested database is created.
--  endpoints_changed: event emitted when the read/write endpoints of the database have changed.
--  read_only_endpoints_changed: event emitted when the read-only endpoints of the database
-  have changed. Event is not triggered if read/write endpoints changed too.
-
-If it is needed to connect multiple database clusters to the same relation endpoint
-the application charm can implement the same code as if it would connect to only
-one database cluster (like the above code example).
-
-To differentiate multiple clusters connected to the same relation endpoint
-the application charm can use the name of the remote application:
-
-```python
-
-def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-    # Get the remote app name of the cluster that triggered this event
-    cluster = event.relation.app.name
-```
-
-It is also possible to provide an alias for each different database cluster/relation.
-
-So, it is possible to differentiate the clusters in two ways.
-The first is to use the remote application name, i.e., `event.relation.app.name`, as above.
-
-The second way is to use different event handlers to handle each cluster events.
-The implementation would be something like the following code:
-
-```python
-
-from charms.data_platform_libs.v0.data_interfaces import (
-    DatabaseCreatedEvent,
-    DatabaseRequires,
-)
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Define the cluster aliases and one handler for each cluster database created event.
-        self.database = DatabaseRequires(
-            self,
-            relation_name="database",
-            database_name="database",
-            relations_aliases = ["cluster1", "cluster2"],
-        )
-        self.framework.observe(
-            self.database.on.cluster1_database_created, self._on_cluster1_database_created
-        )
-        self.framework.observe(
-            self.database.on.cluster2_database_created, self._on_cluster2_database_created
-        )
-
-    def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster1
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-    def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster2
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-```
-
-### Provider Charm
-
-Following an example of using the DatabaseRequestedEvent, in the context of the
-database charm code:
-
-```python
-from charms.data_platform_libs.v0.data_interfaces import DatabaseProvides
-
-class SampleCharm(CharmBase):
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        # Charm events defined in the database provides charm library.
-        self.provided_database = DatabaseProvides(self, relation_name="database")
-        self.framework.observe(self.provided_database.on.database_requested,
-            self._on_database_requested)
-        # Database generic helper
-        self.database = DatabaseHelper()
-
-    def _on_database_requested(self, event: DatabaseRequestedEvent) -> None:
-        # Handle the event triggered by a new database requested in the relation
-        # Retrieve the database name using the charm library.
-        db_name = event.database
-        # generate a new user credential
-        username = self.database.generate_user()
-        password = self.database.generate_password()
-        # set the credentials for the relation
-        self.provided_database.set_credentials(event.relation.id, username, password)
-        # set other variables for the relation event.set_tls("False")
-```
-As shown above, the library provides a custom event (database_requested) to handle
-the situation when an application charm requests a new database to be created.
-It's preferred to subscribe to this event instead of relation changed event to avoid
-creating a new database when other information other than a database name is
-exchanged in the relation databag.
-
-### Kafka
-
-This library is the interface to use and interact with the Kafka charm. This library contains
-custom events that add convenience to manage Kafka, and provides methods to consume the
-application related data.
-
-#### Requirer Charm
-
-```python
-
-from charms.data_platform_libs.v0.data_interfaces import (
-    BootstrapServerChangedEvent,
-    KafkaRequires,
-    TopicCreatedEvent,
-)
-
-class ApplicationCharm(CharmBase):
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.kafka = KafkaRequires(self, "kafka_client", "test-topic")
-        self.framework.observe(
-            self.kafka.on.bootstrap_server_changed, self._on_kafka_bootstrap_server_changed
-        )
-        self.framework.observe(
-            self.kafka.on.topic_created, self._on_kafka_topic_created
-        )
-
-    def _on_kafka_bootstrap_server_changed(self, event: BootstrapServerChangedEvent):
-        # Event triggered when a bootstrap server was changed for this application
-
-        new_bootstrap_server = event.bootstrap_server
-        ...
-
-    def _on_kafka_topic_created(self, event: TopicCreatedEvent):
-        # Event triggered when a topic was created for this application
-        username = event.username
-        password = event.password
-        tls = event.tls
-        tls_ca= event.tls_ca
-        bootstrap_server event.bootstrap_server
-        consumer_group_prefic = event.consumer_group_prefix
-        zookeeper_uris = event.zookeeper_uris
-        ...
-
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
-- topic_created: event emitted when the requested topic is created.
-- bootstrap_server_changed: event emitted when the bootstrap server have changed.
-- credential_changed: event emitted when the credentials of Kafka changed.
-
-### Provider Charm
-
-Following the previous example, this is an example of the provider charm.
-
-```python
-class SampleCharm(CharmBase):
-
-from charms.data_platform_libs.v0.data_interfaces import (
-    KafkaProvides,
-    TopicRequestedEvent,
-)
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Default charm events.
-        self.framework.observe(self.on.start, self._on_start)
-
-        # Charm events defined in the Kafka Provides charm library.
-        self.kafka_provider = KafkaProvides(self, relation_name="kafka_client")
-        self.framework.observe(self.kafka_provider.on.topic_requested, self._on_topic_requested)
-        # Kafka generic helper
-        self.kafka = KafkaHelper()
-
-    def _on_topic_requested(self, event: TopicRequestedEvent):
-        # Handle the on_topic_requested event.
-
-        topic = event.topic
-        relation_id = event.relation.id
-        # set connection info in the databag relation
-        self.kafka_provider.set_bootstrap_server(relation_id, self.kafka.get_bootstrap_server())
-        self.kafka_provider.set_credentials(relation_id, username=username, password=password)
-        self.kafka_provider.set_consumer_group_prefix(relation_id, ...)
-        self.kafka_provider.set_tls(relation_id, "False")
-        self.kafka_provider.set_zookeeper_uris(relation_id, ...)
-
-```
-As shown above, the library provides a custom event (topic_requested) to handle
-the situation when an application charm requests a new topic to be created.
-It is preferred to subscribe to this event instead of relation changed event to avoid
-creating a new topic when other information other than a topic name is
-exchanged in the relation databag.
-"""
-
-import json
-import logging
-from abc import ABC, abstractmethod
-from collections import namedtuple
-from datetime import datetime
-from typing import List, Optional
-
-from ops.charm import (
-    CharmBase,
-    CharmEvents,
-    RelationChangedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-)
-from ops.framework import EventSource, Object
-from ops.model import Relation
-
-# The unique Charmhub library identifier, never change it
-LIBID = "6c3e6b6680d64e9c89e611d1a15f65be"
-
-# 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 = 7
-
-PYDEPS = ["ops>=2.0.0"]
-
-logger = logging.getLogger(__name__)
-
-Diff = namedtuple("Diff", "added changed deleted")
-Diff.__doc__ = """
-A tuple for storing the diff between two data mappings.
-
-added - keys that were added
-changed - keys that still exist but have new values
-deleted - key that were deleted"""
-
-
-def diff(event: RelationChangedEvent, bucket: str) -> Diff:
-    """Retrieves the diff of the data in the relation changed databag.
-
-    Args:
-        event: relation changed event.
-        bucket: bucket of the databag (app or unit)
-
-    Returns:
-        a Diff instance containing the added, deleted and changed
-            keys from the event relation databag.
-    """
-    # Retrieve the old data from the data key in the application relation databag.
-    old_data = json.loads(event.relation.data[bucket].get("data", "{}"))
-    # Retrieve the new data from the event relation databag.
-    new_data = {
-        key: value for key, value in event.relation.data[event.app].items() if key != "data"
-    }
-
-    # These are the keys that were added to the databag and triggered this event.
-    added = new_data.keys() - old_data.keys()
-    # These are the keys that were removed from the databag and triggered this event.
-    deleted = old_data.keys() - new_data.keys()
-    # These are the keys that already existed in the databag,
-    # but had their values changed.
-    changed = {key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]}
-    # Convert the new_data to a serializable format and save it for a next diff check.
-    event.relation.data[bucket].update({"data": json.dumps(new_data)})
-
-    # Return the diff with all possible changes.
-    return Diff(added, changed, deleted)
-
-
-# Base DataProvides and DataRequires
-
-
-class DataProvides(Object, ABC):
-    """Base provides-side of the data products relation."""
-
-    def __init__(self, charm: CharmBase, relation_name: str) -> None:
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.framework.observe(
-            charm.on[relation_name].relation_changed,
-            self._on_relation_changed,
-        )
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        return diff(event, self.local_app)
-
-    @abstractmethod
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the relation data has changed."""
-        raise NotImplementedError
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation id).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = {
-                key: value for key, value in relation.data[relation.app].items() if key != "data"
-            }
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return list(self.charm.model.relations[self.relation_name])
-
-    def set_credentials(self, relation_id: int, username: str, password: str) -> None:
-        """Set credentials.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            username: user that was created.
-            password: password of the created user.
-        """
-        self._update_relation_data(
-            relation_id,
-            {
-                "username": username,
-                "password": password,
-            },
-        )
-
-    def set_tls(self, relation_id: int, tls: str) -> None:
-        """Set whether TLS is enabled.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            tls: whether tls is enabled (True or False).
-        """
-        self._update_relation_data(relation_id, {"tls": tls})
-
-    def set_tls_ca(self, relation_id: int, tls_ca: str) -> None:
-        """Set the TLS CA in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            tls_ca: TLS certification authority.
-        """
-        self._update_relation_data(relation_id, {"tls_ca": tls_ca})
-
-
-class DataRequires(Object, ABC):
-    """Requires-side of the relation."""
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        extra_user_roles: str = None,
-    ):
-        """Manager of base client relations."""
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.extra_user_roles = extra_user_roles
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined, self._on_relation_joined_event
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed, self._on_relation_changed_event
-        )
-
-    @abstractmethod
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the relation."""
-        raise NotImplementedError
-
-    @abstractmethod
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        raise NotImplementedError
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-        Function cannot be used in `*-relation-broken` events and will raise an exception.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation ID).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = {
-                key: value for key, value in relation.data[relation.app].items() if key != "data"
-            }
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        return diff(event, self.local_unit)
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return [
-            relation
-            for relation in self.charm.model.relations[self.relation_name]
-            if self._is_relation_active(relation)
-        ]
-
-    @staticmethod
-    def _is_relation_active(relation: Relation):
-        try:
-            _ = repr(relation.data)
-            return True
-        except RuntimeError:
-            return False
-
-    @staticmethod
-    def _is_resource_created_for_relation(relation: Relation):
-        return (
-            "username" in relation.data[relation.app] and "password" in relation.data[relation.app]
-        )
-
-    def is_resource_created(self, relation_id: Optional[int] = None) -> bool:
-        """Check if the resource has been created.
-
-        This function can be used to check if the Provider answered with data in the charm code
-        when outside an event callback.
-
-        Args:
-            relation_id (int, optional): When provided the check is done only for the relation id
-                provided, otherwise the check is done for all relations
-
-        Returns:
-            True or False
-
-        Raises:
-            IndexError: If relation_id is provided but that relation does not exist
-        """
-        if relation_id is not None:
-            try:
-                relation = [relation for relation in self.relations if relation.id == relation_id][
-                    0
-                ]
-                return self._is_resource_created_for_relation(relation)
-            except IndexError:
-                raise IndexError(f"relation id {relation_id} cannot be accessed")
-        else:
-            return (
-                all(
-                    [
-                        self._is_resource_created_for_relation(relation)
-                        for relation in self.relations
-                    ]
-                )
-                if self.relations
-                else False
-            )
-
-
-# General events
-
-
-class ExtraRoleEvent(RelationEvent):
-    """Base class for data events."""
-
-    @property
-    def extra_user_roles(self) -> Optional[str]:
-        """Returns the extra user roles that were requested."""
-        return self.relation.data[self.relation.app].get("extra-user-roles")
-
-
-class AuthenticationEvent(RelationEvent):
-    """Base class for authentication fields for events."""
-
-    @property
-    def username(self) -> Optional[str]:
-        """Returns the created username."""
-        return self.relation.data[self.relation.app].get("username")
-
-    @property
-    def password(self) -> Optional[str]:
-        """Returns the password for the created user."""
-        return self.relation.data[self.relation.app].get("password")
-
-    @property
-    def tls(self) -> Optional[str]:
-        """Returns whether TLS is configured."""
-        return self.relation.data[self.relation.app].get("tls")
-
-    @property
-    def tls_ca(self) -> Optional[str]:
-        """Returns TLS CA."""
-        return self.relation.data[self.relation.app].get("tls-ca")
-
-
-# Database related events and fields
-
-
-class DatabaseProvidesEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def database(self) -> Optional[str]:
-        """Returns the database that was requested."""
-        return self.relation.data[self.relation.app].get("database")
-
-
-class DatabaseRequestedEvent(DatabaseProvidesEvent, ExtraRoleEvent):
-    """Event emitted when a new database is requested for use on this relation."""
-
-
-class DatabaseProvidesEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_requested = EventSource(DatabaseRequestedEvent)
-
-
-class DatabaseRequiresEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read/write endpoints."""
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def read_only_endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read only endpoints."""
-        return self.relation.data[self.relation.app].get("read-only-endpoints")
-
-    @property
-    def replset(self) -> Optional[str]:
-        """Returns the replicaset name.
-
-        MongoDB only.
-        """
-        return self.relation.data[self.relation.app].get("replset")
-
-    @property
-    def uris(self) -> Optional[str]:
-        """Returns the connection URIs.
-
-        MongoDB, Redis, OpenSearch.
-        """
-        return self.relation.data[self.relation.app].get("uris")
-
-    @property
-    def version(self) -> Optional[str]:
-        """Returns the version of the database.
-
-        Version as informed by the database daemon.
-        """
-        return self.relation.data[self.relation.app].get("version")
-
-
-class DatabaseCreatedEvent(AuthenticationEvent, DatabaseRequiresEvent):
-    """Event emitted when a new database is created for use on this relation."""
-
-
-class DatabaseEndpointsChangedEvent(AuthenticationEvent, DatabaseRequiresEvent):
-    """Event emitted when the read/write endpoints are changed."""
-
-
-class DatabaseReadOnlyEndpointsChangedEvent(AuthenticationEvent, DatabaseRequiresEvent):
-    """Event emitted when the read only endpoints are changed."""
-
-
-class DatabaseRequiresEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_created = EventSource(DatabaseCreatedEvent)
-    endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
-    read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent)
-
-
-# Database Provider and Requires
-
-
-class DatabaseProvides(DataProvides):
-    """Provider-side of the database relations."""
-
-    on = DatabaseProvidesEvents()
-
-    def __init__(self, charm: CharmBase, relation_name: str) -> None:
-        super().__init__(charm, relation_name)
-
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the relation has changed."""
-        # Only the leader should handle this event.
-        if not self.local_unit.is_leader():
-            return
-
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Emit a database requested event if the setup key (database name and optional
-        # extra user roles) was added to the relation databag by the application.
-        if "database" in diff.added:
-            self.on.database_requested.emit(event.relation, app=event.app, unit=event.unit)
-
-    def set_endpoints(self, relation_id: int, connection_strings: str) -> None:
-        """Set database primary connections.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            connection_strings: database hosts and ports comma separated list.
-        """
-        self._update_relation_data(relation_id, {"endpoints": connection_strings})
-
-    def set_read_only_endpoints(self, relation_id: int, connection_strings: str) -> None:
-        """Set database replicas connection strings.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            connection_strings: database hosts and ports comma separated list.
-        """
-        self._update_relation_data(relation_id, {"read-only-endpoints": connection_strings})
-
-    def set_replset(self, relation_id: int, replset: str) -> None:
-        """Set replica set name in the application relation databag.
-
-        MongoDB only.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            replset: replica set name.
-        """
-        self._update_relation_data(relation_id, {"replset": replset})
-
-    def set_uris(self, relation_id: int, uris: str) -> None:
-        """Set the database connection URIs in the application relation databag.
-
-        MongoDB, Redis, and OpenSearch only.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            uris: connection URIs.
-        """
-        self._update_relation_data(relation_id, {"uris": uris})
-
-    def set_version(self, relation_id: int, version: str) -> None:
-        """Set the database version in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            version: database version.
-        """
-        self._update_relation_data(relation_id, {"version": version})
-
-
-class DatabaseRequires(DataRequires):
-    """Requires-side of the database relation."""
-
-    on = DatabaseRequiresEvents()
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        database_name: str,
-        extra_user_roles: str = None,
-        relations_aliases: List[str] = None,
-    ):
-        """Manager of database client relations."""
-        super().__init__(charm, relation_name, extra_user_roles)
-        self.database = database_name
-        self.relations_aliases = relations_aliases
-
-        # Define custom event names for each alias.
-        if relations_aliases:
-            # Ensure the number of aliases does not exceed the maximum
-            # of connections allowed in the specific relation.
-            relation_connection_limit = self.charm.meta.requires[relation_name].limit
-            if len(relations_aliases) != relation_connection_limit:
-                raise ValueError(
-                    f"The number of aliases must match the maximum number of connections allowed in the relation. "
-                    f"Expected {relation_connection_limit}, got {len(relations_aliases)}"
-                )
-
-            for relation_alias in relations_aliases:
-                self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent)
-                self.on.define_event(
-                    f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent
-                )
-                self.on.define_event(
-                    f"{relation_alias}_read_only_endpoints_changed",
-                    DatabaseReadOnlyEndpointsChangedEvent,
-                )
-
-    def _assign_relation_alias(self, relation_id: int) -> None:
-        """Assigns an alias to a relation.
-
-        This function writes in the unit data bag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-        """
-        # If no aliases were provided, return immediately.
-        if not self.relations_aliases:
-            return
-
-        # Return if an alias was already assigned to this relation
-        # (like when there are more than one unit joining the relation).
-        if (
-            self.charm.model.get_relation(self.relation_name, relation_id)
-            .data[self.local_unit]
-            .get("alias")
-        ):
-            return
-
-        # Retrieve the available aliases (the ones that weren't assigned to any relation).
-        available_aliases = self.relations_aliases[:]
-        for relation in self.charm.model.relations[self.relation_name]:
-            alias = relation.data[self.local_unit].get("alias")
-            if alias:
-                logger.debug("Alias %s was already assigned to relation %d", alias, relation.id)
-                available_aliases.remove(alias)
-
-        # Set the alias in the unit relation databag of the specific relation.
-        relation = self.charm.model.get_relation(self.relation_name, relation_id)
-        relation.data[self.local_unit].update({"alias": available_aliases[0]})
-
-    def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None:
-        """Emit an aliased event to a particular relation if it has an alias.
-
-        Args:
-            event: the relation changed event that was received.
-            event_name: the name of the event to emit.
-        """
-        alias = self._get_relation_alias(event.relation.id)
-        if alias:
-            getattr(self.on, f"{alias}_{event_name}").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-    def _get_relation_alias(self, relation_id: int) -> Optional[str]:
-        """Returns the relation alias.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-
-        Returns:
-            the relation alias or None if the relation was not found.
-        """
-        for relation in self.charm.model.relations[self.relation_name]:
-            if relation.id == relation_id:
-                return relation.data[self.local_unit].get("alias")
-        return None
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the database relation."""
-        # If relations aliases were provided, assign one to the relation.
-        self._assign_relation_alias(event.relation.id)
-
-        # Sets both database and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the database.
-        if self.extra_user_roles:
-            self._update_relation_data(
-                event.relation.id,
-                {
-                    "database": self.database,
-                    "extra-user-roles": self.extra_user_roles,
-                },
-            )
-        else:
-            self._update_relation_data(event.relation.id, {"database": self.database})
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the database relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the database is created
-        # (the database charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("database created at %s", datetime.now())
-            self.on.database_created.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "database_created")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “database_created“ is triggered.
-            return
-
-        # Emit an endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "endpoints_changed")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered.
-            return
-
-        # Emit a read only endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("read-only-endpoints changed on %s", datetime.now())
-            self.on.read_only_endpoints_changed.emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "read_only_endpoints_changed")
-
-
-# Kafka related events
-
-
-class KafkaProvidesEvent(RelationEvent):
-    """Base class for Kafka events."""
-
-    @property
-    def topic(self) -> Optional[str]:
-        """Returns the topic that was requested."""
-        return self.relation.data[self.relation.app].get("topic")
-
-
-class TopicRequestedEvent(KafkaProvidesEvent, ExtraRoleEvent):
-    """Event emitted when a new topic is requested for use on this relation."""
-
-
-class KafkaProvidesEvents(CharmEvents):
-    """Kafka events.
-
-    This class defines the events that the Kafka can emit.
-    """
-
-    topic_requested = EventSource(TopicRequestedEvent)
-
-
-class KafkaRequiresEvent(RelationEvent):
-    """Base class for Kafka events."""
-
-    @property
-    def bootstrap_server(self) -> Optional[str]:
-        """Returns a a comma-seperated list of broker uris."""
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def consumer_group_prefix(self) -> Optional[str]:
-        """Returns the consumer-group-prefix."""
-        return self.relation.data[self.relation.app].get("consumer-group-prefix")
-
-    @property
-    def zookeeper_uris(self) -> Optional[str]:
-        """Returns a comma separated list of Zookeeper uris."""
-        return self.relation.data[self.relation.app].get("zookeeper-uris")
-
-
-class TopicCreatedEvent(AuthenticationEvent, KafkaRequiresEvent):
-    """Event emitted when a new topic is created for use on this relation."""
-
-
-class BootstrapServerChangedEvent(AuthenticationEvent, KafkaRequiresEvent):
-    """Event emitted when the bootstrap server is changed."""
-
-
-class KafkaRequiresEvents(CharmEvents):
-    """Kafka events.
-
-    This class defines the events that the Kafka can emit.
-    """
-
-    topic_created = EventSource(TopicCreatedEvent)
-    bootstrap_server_changed = EventSource(BootstrapServerChangedEvent)
-
-
-# Kafka Provides and Requires
-
-
-class KafkaProvides(DataProvides):
-    """Provider-side of the Kafka relation."""
-
-    on = KafkaProvidesEvents()
-
-    def __init__(self, charm: CharmBase, relation_name: str) -> None:
-        super().__init__(charm, relation_name)
-
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the relation has changed."""
-        # Only the leader should handle this event.
-        if not self.local_unit.is_leader():
-            return
-
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Emit a topic requested event if the setup key (topic name and optional
-        # extra user roles) was added to the relation databag by the application.
-        if "topic" in diff.added:
-            self.on.topic_requested.emit(event.relation, app=event.app, unit=event.unit)
-
-    def set_bootstrap_server(self, relation_id: int, bootstrap_server: str) -> None:
-        """Set the bootstrap server in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            bootstrap_server: the bootstrap server address.
-        """
-        self._update_relation_data(relation_id, {"endpoints": bootstrap_server})
-
-    def set_consumer_group_prefix(self, relation_id: int, consumer_group_prefix: str) -> None:
-        """Set the consumer group prefix in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            consumer_group_prefix: the consumer group prefix string.
-        """
-        self._update_relation_data(relation_id, {"consumer-group-prefix": consumer_group_prefix})
-
-    def set_zookeeper_uris(self, relation_id: int, zookeeper_uris: str) -> None:
-        """Set the zookeeper uris in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            zookeeper_uris: comma-seperated list of ZooKeeper server uris.
-        """
-        self._update_relation_data(relation_id, {"zookeeper-uris": zookeeper_uris})
-
-
-class KafkaRequires(DataRequires):
-    """Requires-side of the Kafka relation."""
-
-    on = KafkaRequiresEvents()
-
-    def __init__(self, charm, relation_name: str, topic: str, extra_user_roles: str = None):
-        """Manager of Kafka client relations."""
-        # super().__init__(charm, relation_name)
-        super().__init__(charm, relation_name, extra_user_roles)
-        self.charm = charm
-        self.topic = topic
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the Kafka relation."""
-        # Sets both topic and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the topic.
-        self._update_relation_data(
-            event.relation.id,
-            {
-                "topic": self.topic,
-                "extra-user-roles": self.extra_user_roles,
-            }
-            if self.extra_user_roles is not None
-            else {"topic": self.topic},
-        )
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the Kafka relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the topic is created
-        # (the Kafka charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("topic created at %s", datetime.now())
-            self.on.topic_created.emit(event.relation, app=event.app, unit=event.unit)
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “topic_created“ is triggered.
-            return
-
-        # Emit an endpoints (bootstap-server) changed event if the Kakfa endpoints
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            self.on.bootstrap_server_changed.emit(
-                event.relation, app=event.app, unit=event.unit
-            )  # here check if this is the right design
-            return
diff --git a/installers/charm/osm-mon/lib/charms/kafka_k8s/v0/kafka.py b/installers/charm/osm-mon/lib/charms/kafka_k8s/v0/kafka.py
deleted file mode 100644 (file)
index aeb5edc..0000000
+++ /dev/null
@@ -1,200 +0,0 @@
-# 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.
-
-"""Kafka library.
-
-This [library](https://juju.is/docs/sdk/libraries) implements both sides of the
-`kafka` [interface](https://juju.is/docs/sdk/relations).
-
-The *provider* side of this interface is implemented by the
-[kafka-k8s Charmed Operator](https://charmhub.io/kafka-k8s).
-
-Any Charmed Operator that *requires* Kafka for providing its
-service should implement the *requirer* side of this interface.
-
-In a nutshell using this library to implement a Charmed Operator *requiring*
-Kafka would look like
-
-```
-$ charmcraft fetch-lib charms.kafka_k8s.v0.kafka
-```
-
-`metadata.yaml`:
-
-```
-requires:
-  kafka:
-    interface: kafka
-    limit: 1
-```
-
-`src/charm.py`:
-
-```
-from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
-from ops.charm import CharmBase
-
-
-class MyCharm(CharmBase):
-
-    on = KafkaEvents()
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.kafka = KafkaRequires(self)
-        self.framework.observe(
-            self.on.kafka_available,
-            self._on_kafka_available,
-        )
-        self.framework.observe(
-            self.on["kafka"].relation_broken,
-            self._on_kafka_broken,
-        )
-
-    def _on_kafka_available(self, event):
-        # Get Kafka host and port
-        host: str = self.kafka.host
-        port: int = self.kafka.port
-        # host => "kafka-k8s"
-        # port => 9092
-
-    def _on_kafka_broken(self, event):
-        # Stop service
-        # ...
-        self.unit.status = BlockedStatus("need kafka relation")
-```
-
-You can file bugs
-[here](https://github.com/charmed-osm/kafka-k8s-operator/issues)!
-"""
-
-from typing import Optional
-
-from ops.charm import CharmBase, CharmEvents
-from ops.framework import EventBase, EventSource, Object
-
-# The unique Charmhub library identifier, never change it
-from ops.model import Relation
-
-LIBID = "eacc8c85082347c9aae740e0220b8376"
-
-# 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 = 4
-
-
-KAFKA_HOST_APP_KEY = "host"
-KAFKA_PORT_APP_KEY = "port"
-
-
-class _KafkaAvailableEvent(EventBase):
-    """Event emitted when Kafka is available."""
-
-
-class KafkaEvents(CharmEvents):
-    """Kafka events.
-
-    This class defines the events that Kafka can emit.
-
-    Events:
-        kafka_available (_KafkaAvailableEvent)
-    """
-
-    kafka_available = EventSource(_KafkaAvailableEvent)
-
-
-class KafkaRequires(Object):
-    """Requires-side of the Kafka relation."""
-
-    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
-        super().__init__(charm, endpoint_name)
-        self.charm = charm
-        self._endpoint_name = endpoint_name
-
-        # Observe relation events
-        event_observe_mapping = {
-            charm.on[self._endpoint_name].relation_changed: self._on_relation_changed,
-        }
-        for event, observer in event_observe_mapping.items():
-            self.framework.observe(event, observer)
-
-    def _on_relation_changed(self, event) -> None:
-        if event.relation.app and all(
-            key in event.relation.data[event.relation.app]
-            for key in (KAFKA_HOST_APP_KEY, KAFKA_PORT_APP_KEY)
-        ):
-            self.charm.on.kafka_available.emit()
-
-    @property
-    def host(self) -> str:
-        """Get kafka hostname."""
-        relation: Relation = self.model.get_relation(self._endpoint_name)
-        return (
-            relation.data[relation.app].get(KAFKA_HOST_APP_KEY)
-            if relation and relation.app
-            else None
-        )
-
-    @property
-    def port(self) -> int:
-        """Get kafka port number."""
-        relation: Relation = self.model.get_relation(self._endpoint_name)
-        return (
-            int(relation.data[relation.app].get(KAFKA_PORT_APP_KEY))
-            if relation and relation.app
-            else None
-        )
-
-
-class KafkaProvides(Object):
-    """Provides-side of the Kafka relation."""
-
-    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
-        super().__init__(charm, endpoint_name)
-        self._endpoint_name = endpoint_name
-
-    def set_host_info(self, host: str, port: int, relation: Optional[Relation] = None) -> None:
-        """Set Kafka host and port.
-
-        This function writes in the application data of the relation, therefore,
-        only the unit leader can call it.
-
-        Args:
-            host (str): Kafka hostname or IP address.
-            port (int): Kafka port.
-            relation (Optional[Relation]): Relation to update.
-                                           If not specified, all relations will be updated.
-
-        Raises:
-            Exception: if a non-leader unit calls this function.
-        """
-        if not self.model.unit.is_leader():
-            raise Exception("only the leader set host information.")
-
-        if relation:
-            self._update_relation_data(host, port, relation)
-            return
-
-        for relation in self.model.relations[self._endpoint_name]:
-            self._update_relation_data(host, port, relation)
-
-    def _update_relation_data(self, host: str, port: int, relation: Relation) -> None:
-        """Update data in relation if needed."""
-        relation.data[self.model.app][KAFKA_HOST_APP_KEY] = host
-        relation.data[self.model.app][KAFKA_PORT_APP_KEY] = str(port)
diff --git a/installers/charm/osm-mon/lib/charms/observability_libs/v1/kubernetes_service_patch.py b/installers/charm/osm-mon/lib/charms/observability_libs/v1/kubernetes_service_patch.py
deleted file mode 100644 (file)
index 506dbf0..0000000
+++ /dev/null
@@ -1,291 +0,0 @@
-# Copyright 2021 Canonical Ltd.
-# See LICENSE file for licensing details.
-#   http://www.apache.org/licenses/LICENSE-2.0
-
-"""# KubernetesServicePatch Library.
-
-This library is designed to enable developers to more simply patch the Kubernetes Service created
-by Juju during the deployment of a sidecar charm. When sidecar charms are deployed, Juju creates a
-service named after the application in the namespace (named after the Juju model). This service by
-default contains a "placeholder" port, which is 65536/TCP.
-
-When modifying the default set of resources managed by Juju, one must consider the lifecycle of the
-charm. In this case, any modifications to the default service (created during deployment), will be
-overwritten during a charm upgrade.
-
-When initialised, this library binds a handler to the parent charm's `install` and `upgrade_charm`
-events which applies the patch to the cluster. This should ensure that the service ports are
-correct throughout the charm's life.
-
-The constructor simply takes a reference to the parent charm, and a list of
-[`lightkube`](https://github.com/gtsystem/lightkube) ServicePorts that each define a port for the
-service. For information regarding the `lightkube` `ServicePort` model, please visit the
-`lightkube` [docs](https://gtsystem.github.io/lightkube-models/1.23/models/core_v1/#serviceport).
-
-Optionally, a name of the service (in case service name needs to be patched as well), labels,
-selectors, and annotations can be provided as keyword arguments.
-
-## Getting Started
-
-To get started using the library, you just need to fetch the library using `charmcraft`. **Note
-that you also need to add `lightkube` and `lightkube-models` to your charm's `requirements.txt`.**
-
-```shell
-cd some-charm
-charmcraft fetch-lib charms.observability_libs.v0.kubernetes_service_patch
-echo <<-EOF >> requirements.txt
-lightkube
-lightkube-models
-EOF
-```
-
-Then, to initialise the library:
-
-For `ClusterIP` services:
-
-```python
-# ...
-from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
-from lightkube.models.core_v1 import ServicePort
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    port = ServicePort(443, name=f"{self.app.name}")
-    self.service_patcher = KubernetesServicePatch(self, [port])
-    # ...
-```
-
-For `LoadBalancer`/`NodePort` services:
-
-```python
-# ...
-from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
-from lightkube.models.core_v1 import ServicePort
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    port = ServicePort(443, name=f"{self.app.name}", targetPort=443, nodePort=30666)
-    self.service_patcher = KubernetesServicePatch(
-        self, [port], "LoadBalancer"
-    )
-    # ...
-```
-
-Port protocols can also be specified. Valid protocols are `"TCP"`, `"UDP"`, and `"SCTP"`
-
-```python
-# ...
-from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
-from lightkube.models.core_v1 import ServicePort
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    tcp = ServicePort(443, name=f"{self.app.name}-tcp", protocol="TCP")
-    udp = ServicePort(443, name=f"{self.app.name}-udp", protocol="UDP")
-    sctp = ServicePort(443, name=f"{self.app.name}-sctp", protocol="SCTP")
-    self.service_patcher = KubernetesServicePatch(self, [tcp, udp, sctp])
-    # ...
-```
-
-Additionally, you may wish to use mocks in your charm's unit testing to ensure that the library
-does not try to make any API calls, or open any files during testing that are unlikely to be
-present, and could break your tests. The easiest way to do this is during your test `setUp`:
-
-```python
-# ...
-
-@patch("charm.KubernetesServicePatch", lambda x, y: None)
-def setUp(self, *unused):
-    self.harness = Harness(SomeCharm)
-    # ...
-```
-"""
-
-import logging
-from types import MethodType
-from typing import List, Literal
-
-from lightkube import ApiError, Client
-from lightkube.models.core_v1 import ServicePort, ServiceSpec
-from lightkube.models.meta_v1 import ObjectMeta
-from lightkube.resources.core_v1 import Service
-from lightkube.types import PatchType
-from ops.charm import CharmBase
-from ops.framework import Object
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0042f86d0a874435adef581806cddbbb"
-
-# 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 = 1
-
-ServiceType = Literal["ClusterIP", "LoadBalancer"]
-
-
-class KubernetesServicePatch(Object):
-    """A utility for patching the Kubernetes service set up by Juju."""
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        ports: List[ServicePort],
-        service_name: str = None,
-        service_type: ServiceType = "ClusterIP",
-        additional_labels: dict = None,
-        additional_selectors: dict = None,
-        additional_annotations: dict = None,
-    ):
-        """Constructor for KubernetesServicePatch.
-
-        Args:
-            charm: the charm that is instantiating the library.
-            ports: a list of ServicePorts
-            service_name: allows setting custom name to the patched service. If none given,
-                application name will be used.
-            service_type: desired type of K8s service. Default value is in line with ServiceSpec's
-                default value.
-            additional_labels: Labels to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_selectors: Selectors to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_annotations: Annotations to be added to the kubernetes service.
-        """
-        super().__init__(charm, "kubernetes-service-patch")
-        self.charm = charm
-        self.service_name = service_name if service_name else self._app
-        self.service = self._service_object(
-            ports,
-            service_name,
-            service_type,
-            additional_labels,
-            additional_selectors,
-            additional_annotations,
-        )
-
-        # Make mypy type checking happy that self._patch is a method
-        assert isinstance(self._patch, MethodType)
-        # Ensure this patch is applied during the 'install' and 'upgrade-charm' events
-        self.framework.observe(charm.on.install, self._patch)
-        self.framework.observe(charm.on.upgrade_charm, self._patch)
-
-    def _service_object(
-        self,
-        ports: List[ServicePort],
-        service_name: str = None,
-        service_type: ServiceType = "ClusterIP",
-        additional_labels: dict = None,
-        additional_selectors: dict = None,
-        additional_annotations: dict = None,
-    ) -> Service:
-        """Creates a valid Service representation.
-
-        Args:
-            ports: a list of ServicePorts
-            service_name: allows setting custom name to the patched service. If none given,
-                application name will be used.
-            service_type: desired type of K8s service. Default value is in line with ServiceSpec's
-                default value.
-            additional_labels: Labels to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_selectors: Selectors to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_annotations: Annotations to be added to the kubernetes service.
-
-        Returns:
-            Service: A valid representation of a Kubernetes Service with the correct ports.
-        """
-        if not service_name:
-            service_name = self._app
-        labels = {"app.kubernetes.io/name": self._app}
-        if additional_labels:
-            labels.update(additional_labels)
-        selector = {"app.kubernetes.io/name": self._app}
-        if additional_selectors:
-            selector.update(additional_selectors)
-        return Service(
-            apiVersion="v1",
-            kind="Service",
-            metadata=ObjectMeta(
-                namespace=self._namespace,
-                name=service_name,
-                labels=labels,
-                annotations=additional_annotations,  # type: ignore[arg-type]
-            ),
-            spec=ServiceSpec(
-                selector=selector,
-                ports=ports,
-                type=service_type,
-            ),
-        )
-
-    def _patch(self, _) -> None:
-        """Patch the Kubernetes service created by Juju to map the correct port.
-
-        Raises:
-            PatchFailed: if patching fails due to lack of permissions, or otherwise.
-        """
-        if not self.charm.unit.is_leader():
-            return
-
-        client = Client()
-        try:
-            if self.service_name != self._app:
-                self._delete_and_create_service(client)
-            client.patch(Service, self.service_name, self.service, patch_type=PatchType.MERGE)
-        except ApiError as e:
-            if e.status.code == 403:
-                logger.error("Kubernetes service patch failed: `juju trust` this application.")
-            else:
-                logger.error("Kubernetes service patch failed: %s", str(e))
-        else:
-            logger.info("Kubernetes service '%s' patched successfully", self._app)
-
-    def _delete_and_create_service(self, client: Client):
-        service = client.get(Service, self._app, namespace=self._namespace)
-        service.metadata.name = self.service_name  # type: ignore[attr-defined]
-        service.metadata.resourceVersion = service.metadata.uid = None  # type: ignore[attr-defined]   # noqa: E501
-        client.delete(Service, self._app, namespace=self._namespace)
-        client.create(service)
-
-    def is_patched(self) -> bool:
-        """Reports if the service patch has been applied.
-
-        Returns:
-            bool: A boolean indicating if the service patch has been applied.
-        """
-        client = Client()
-        # Get the relevant service from the cluster
-        service = client.get(Service, name=self.service_name, namespace=self._namespace)
-        # Construct a list of expected ports, should the patch be applied
-        expected_ports = [(p.port, p.targetPort) for p in self.service.spec.ports]
-        # Construct a list in the same manner, using the fetched service
-        fetched_ports = [(p.port, p.targetPort) for p in service.spec.ports]  # type: ignore[attr-defined]  # noqa: E501
-        return expected_ports == fetched_ports
-
-    @property
-    def _app(self) -> str:
-        """Name of the current Juju application.
-
-        Returns:
-            str: A string containing the name of the current Juju application.
-        """
-        return self.charm.app.name
-
-    @property
-    def _namespace(self) -> str:
-        """The Kubernetes namespace we're running in.
-
-        Returns:
-            str: A string containing the name of the current Kubernetes namespace.
-        """
-        with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") as f:
-            return f.read().strip()
diff --git a/installers/charm/osm-mon/lib/charms/osm_libs/v0/utils.py b/installers/charm/osm-mon/lib/charms/osm_libs/v0/utils.py
deleted file mode 100644 (file)
index d739ba6..0000000
+++ /dev/null
@@ -1,544 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-#         http://www.apache.org/licenses/LICENSE-2.0
-"""OSM Utils Library.
-
-This library offers some utilities made for but not limited to Charmed OSM.
-
-# Getting started
-
-Execute the following command inside your Charmed Operator folder to fetch the library.
-
-```shell
-charmcraft fetch-lib charms.osm_libs.v0.utils
-```
-
-# CharmError Exception
-
-An exception that takes to arguments, the message and the StatusBase class, which are useful
-to set the status of the charm when the exception raises.
-
-Example:
-```shell
-from charms.osm_libs.v0.utils import CharmError
-
-class MyCharm(CharmBase):
-    def _on_config_changed(self, _):
-        try:
-            if not self.config.get("some-option"):
-                raise CharmError("need some-option", BlockedStatus)
-
-            if not self.mysql_ready:
-                raise CharmError("waiting for mysql", WaitingStatus)
-
-            # Do stuff...
-
-        exception CharmError as e:
-            self.unit.status = e.status
-```
-
-# Pebble validations
-
-The `check_container_ready` function checks that a container is ready,
-and therefore Pebble is ready.
-
-The `check_service_active` function checks that a service in a container is running.
-
-Both functions raise a CharmError if the validations fail.
-
-Example:
-```shell
-from charms.osm_libs.v0.utils import check_container_ready, check_service_active
-
-class MyCharm(CharmBase):
-    def _on_config_changed(self, _):
-        try:
-            container: Container = self.unit.get_container("my-container")
-            check_container_ready(container)
-            check_service_active(container, "my-service")
-            # Do stuff...
-
-        exception CharmError as e:
-            self.unit.status = e.status
-```
-
-# Debug-mode
-
-The debug-mode allows OSM developers to easily debug OSM modules.
-
-Example:
-```shell
-from charms.osm_libs.v0.utils import DebugMode
-
-class MyCharm(CharmBase):
-    _stored = StoredState()
-
-    def __init__(self, _):
-        # ...
-        container: Container = self.unit.get_container("my-container")
-        hostpaths = [
-            HostPath(
-                config="module-hostpath",
-                container_path="/usr/lib/python3/dist-packages/module"
-            ),
-        ]
-        vscode_workspace_path = "files/vscode-workspace.json"
-        self.debug_mode = DebugMode(
-            self,
-            self._stored,
-            container,
-            hostpaths,
-            vscode_workspace_path,
-        )
-
-    def _on_update_status(self, _):
-        if self.debug_mode.started:
-            return
-        # ...
-
-    def _get_debug_mode_information(self):
-        command = self.debug_mode.command
-        password = self.debug_mode.password
-        return command, password
-```
-
-# More
-
-- Get pod IP with `get_pod_ip()`
-"""
-from dataclasses import dataclass
-import logging
-import secrets
-import socket
-from pathlib import Path
-from typing import List
-
-from lightkube import Client
-from lightkube.models.core_v1 import HostPathVolumeSource, Volume, VolumeMount
-from lightkube.resources.apps_v1 import StatefulSet
-from ops.charm import CharmBase
-from ops.framework import Object, StoredState
-from ops.model import (
-    ActiveStatus,
-    BlockedStatus,
-    Container,
-    MaintenanceStatus,
-    StatusBase,
-    WaitingStatus,
-)
-from ops.pebble import ServiceStatus
-
-# The unique Charmhub library identifier, never change it
-LIBID = "e915908eebee4cdd972d484728adf984"
-
-# 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 = 5
-
-logger = logging.getLogger(__name__)
-
-
-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)
-
-
-def check_container_ready(container: Container) -> None:
-    """Check Pebble has started in the container.
-
-    Args:
-        container (Container): Container to be checked.
-
-    Raises:
-        CharmError: if container is not ready.
-    """
-    if not container.can_connect():
-        raise CharmError("waiting for pebble to start", MaintenanceStatus)
-
-
-def check_service_active(container: Container, service_name: str) -> None:
-    """Check if the service is running.
-
-    Args:
-        container (Container): Container to be checked.
-        service_name (str): Name of the service to check.
-
-    Raises:
-        CharmError: if the service is not running.
-    """
-    if service_name not in container.get_plan().services:
-        raise CharmError(f"{service_name} service not configured yet", WaitingStatus)
-
-    if container.get_service(service_name).current != ServiceStatus.ACTIVE:
-        raise CharmError(f"{service_name} service is not running")
-
-
-def get_pod_ip() -> str:
-    """Get Kubernetes Pod IP.
-
-    Returns:
-        str: The IP of the Pod.
-    """
-    return socket.gethostbyname(socket.gethostname())
-
-
-_DEBUG_SCRIPT = r"""#!/bin/bash
-# Install SSH
-
-function download_code(){{
-    wget https://go.microsoft.com/fwlink/?LinkID=760868 -O code.deb
-}}
-
-function setup_envs(){{
-    grep "source /debug.envs" /root/.bashrc || echo "source /debug.envs" | tee -a /root/.bashrc
-}}
-function setup_ssh(){{
-    apt install ssh -y
-    cat /etc/ssh/sshd_config |
-        grep -E '^PermitRootLogin yes$$' || (
-        echo PermitRootLogin yes |
-        tee -a /etc/ssh/sshd_config
-    )
-    service ssh stop
-    sleep 3
-    service ssh start
-    usermod --password $(echo {} | openssl passwd -1 -stdin) root
-}}
-
-function setup_code(){{
-    apt install libasound2 -y
-    (dpkg -i code.deb || apt-get install -f -y || apt-get install -f -y) && echo Code installed successfully
-    code --install-extension ms-python.python --user-data-dir /root
-    mkdir -p /root/.vscode-server
-    cp -R /root/.vscode/extensions /root/.vscode-server/extensions
-}}
-
-export DEBIAN_FRONTEND=noninteractive
-apt update && apt install wget -y
-download_code &
-setup_ssh &
-setup_envs
-wait
-setup_code &
-wait
-"""
-
-
-@dataclass
-class SubModule:
-    """Represent RO Submodules."""
-    sub_module_path: str
-    container_path: str
-
-
-class HostPath:
-    """Represents a hostpath."""
-    def __init__(self, config: str, container_path: str, submodules: dict = None) -> None:
-        mount_path_items = config.split("-")
-        mount_path_items.reverse()
-        self.mount_path = "/" + "/".join(mount_path_items)
-        self.config = config
-        self.sub_module_dict = {}
-        if submodules:
-            for submodule in submodules.keys():
-                self.sub_module_dict[submodule] = SubModule(
-                    sub_module_path=self.mount_path + "/" + submodule + "/" + submodules[submodule].split("/")[-1],
-                    container_path=submodules[submodule],
-                )
-        else:
-            self.container_path = container_path
-            self.module_name = container_path.split("/")[-1]
-
-class DebugMode(Object):
-    """Class to handle the debug-mode."""
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        stored: StoredState,
-        container: Container,
-        hostpaths: List[HostPath] = [],
-        vscode_workspace_path: str = "files/vscode-workspace.json",
-    ) -> None:
-        super().__init__(charm, "debug-mode")
-
-        self.charm = charm
-        self._stored = stored
-        self.hostpaths = hostpaths
-        self.vscode_workspace = Path(vscode_workspace_path).read_text()
-        self.container = container
-
-        self._stored.set_default(
-            debug_mode_started=False,
-            debug_mode_vscode_command=None,
-            debug_mode_password=None,
-        )
-
-        self.framework.observe(self.charm.on.config_changed, self._on_config_changed)
-        self.framework.observe(self.charm.on[container.name].pebble_ready, self._on_config_changed)
-        self.framework.observe(self.charm.on.update_status, self._on_update_status)
-
-    def _on_config_changed(self, _) -> None:
-        """Handler for the config-changed event."""
-        if not self.charm.unit.is_leader():
-            return
-
-        debug_mode_enabled = self.charm.config.get("debug-mode", False)
-        action = self.enable if debug_mode_enabled else self.disable
-        action()
-
-    def _on_update_status(self, _) -> None:
-        """Handler for the update-status event."""
-        if not self.charm.unit.is_leader() or not self.started:
-            return
-
-        self.charm.unit.status = ActiveStatus("debug-mode: ready")
-
-    @property
-    def started(self) -> bool:
-        """Indicates whether the debug-mode has started or not."""
-        return self._stored.debug_mode_started
-
-    @property
-    def command(self) -> str:
-        """Command to launch vscode."""
-        return self._stored.debug_mode_vscode_command
-
-    @property
-    def password(self) -> str:
-        """SSH password."""
-        return self._stored.debug_mode_password
-
-    def enable(self, service_name: str = None) -> None:
-        """Enable debug-mode.
-
-        This function mounts hostpaths of the OSM modules (if set), and
-        configures the container so it can be easily debugged. The setup
-        includes the configuration of SSH, environment variables, and
-        VSCode workspace and plugins.
-
-        Args:
-            service_name (str, optional): Pebble service name which has the desired environment
-                variables. Mandatory if there is more than one Pebble service configured.
-        """
-        hostpaths_to_reconfigure = self._hostpaths_to_reconfigure()
-        if self.started and not hostpaths_to_reconfigure:
-            self.charm.unit.status = ActiveStatus("debug-mode: ready")
-            return
-
-        logger.debug("enabling debug-mode")
-
-        # Mount hostpaths if set.
-        # If hostpaths are mounted, the statefulset will be restarted,
-        # and for that reason we return immediately. On restart, the hostpaths
-        # won't be mounted and then we can continue and setup the debug-mode.
-        if hostpaths_to_reconfigure:
-            self.charm.unit.status = MaintenanceStatus("debug-mode: configuring hostpaths")
-            self._configure_hostpaths(hostpaths_to_reconfigure)
-            return
-
-        self.charm.unit.status = MaintenanceStatus("debug-mode: starting")
-        password = secrets.token_hex(8)
-        self._setup_debug_mode(
-            password,
-            service_name,
-            mounted_hostpaths=[hp for hp in self.hostpaths if self.charm.config.get(hp.config)],
-        )
-
-        self._stored.debug_mode_vscode_command = self._get_vscode_command(get_pod_ip())
-        self._stored.debug_mode_password = password
-        self._stored.debug_mode_started = True
-        logger.info("debug-mode is ready")
-        self.charm.unit.status = ActiveStatus("debug-mode: ready")
-
-    def disable(self) -> None:
-        """Disable debug-mode."""
-        logger.debug("disabling debug-mode")
-        current_status = self.charm.unit.status
-        hostpaths_unmounted = self._unmount_hostpaths()
-
-        if not self._stored.debug_mode_started:
-            return
-        self._stored.debug_mode_started = False
-        self._stored.debug_mode_vscode_command = None
-        self._stored.debug_mode_password = None
-
-        if not hostpaths_unmounted:
-            self.charm.unit.status = current_status
-            self._restart()
-
-    def _hostpaths_to_reconfigure(self) -> List[HostPath]:
-        hostpaths_to_reconfigure: List[HostPath] = []
-        client = Client()
-        statefulset = client.get(StatefulSet, self.charm.app.name, namespace=self.charm.model.name)
-        volumes = statefulset.spec.template.spec.volumes
-
-        for hostpath in self.hostpaths:
-            hostpath_is_set = True if self.charm.config.get(hostpath.config) else False
-            hostpath_already_configured = next(
-                (True for volume in volumes if volume.name == hostpath.config), False
-            )
-            if hostpath_is_set != hostpath_already_configured:
-                hostpaths_to_reconfigure.append(hostpath)
-
-        return hostpaths_to_reconfigure
-
-    def _setup_debug_mode(
-        self,
-        password: str,
-        service_name: str = None,
-        mounted_hostpaths: List[HostPath] = [],
-    ) -> None:
-        services = self.container.get_plan().services
-        if not service_name and len(services) != 1:
-            raise Exception("Cannot start debug-mode: please set the service_name")
-
-        service = None
-        if not service_name:
-            service_name, service = services.popitem()
-        if not service:
-            service = services.get(service_name)
-
-        logger.debug(f"getting environment variables from service {service_name}")
-        environment = service.environment
-        environment_file_content = "\n".join(
-            [f'export {key}="{value}"' for key, value in environment.items()]
-        )
-        logger.debug(f"pushing environment file to {self.container.name} container")
-        self.container.push("/debug.envs", environment_file_content)
-
-        # Push VSCode workspace
-        logger.debug(f"pushing vscode workspace to {self.container.name} container")
-        self.container.push("/debug.code-workspace", self.vscode_workspace)
-
-        # Execute debugging script
-        logger.debug(f"pushing debug-mode setup script to {self.container.name} container")
-        self.container.push("/debug.sh", _DEBUG_SCRIPT.format(password), permissions=0o777)
-        logger.debug(f"executing debug-mode setup script in {self.container.name} container")
-        self.container.exec(["/debug.sh"]).wait_output()
-        logger.debug(f"stopping service {service_name} in {self.container.name} container")
-        self.container.stop(service_name)
-
-        # Add symlinks to mounted hostpaths
-        for hostpath in mounted_hostpaths:
-            logger.debug(f"adding symlink for {hostpath.config}")
-            if len(hostpath.sub_module_dict) > 0:
-                for sub_module in hostpath.sub_module_dict.keys():
-                    self.container.exec(["rm", "-rf", hostpath.sub_module_dict[sub_module].container_path]).wait_output()
-                    self.container.exec(
-                        [
-                            "ln",
-                            "-s",
-                            hostpath.sub_module_dict[sub_module].sub_module_path,
-                            hostpath.sub_module_dict[sub_module].container_path,
-                        ]
-                    )
-
-            else:
-                self.container.exec(["rm", "-rf", hostpath.container_path]).wait_output()
-                self.container.exec(
-                    [
-                        "ln",
-                        "-s",
-                        f"{hostpath.mount_path}/{hostpath.module_name}",
-                        hostpath.container_path,
-                    ]
-                )
-
-    def _configure_hostpaths(self, hostpaths: List[HostPath]):
-        client = Client()
-        statefulset = client.get(StatefulSet, self.charm.app.name, namespace=self.charm.model.name)
-
-        for hostpath in hostpaths:
-            if self.charm.config.get(hostpath.config):
-                self._add_hostpath_to_statefulset(hostpath, statefulset)
-            else:
-                self._delete_hostpath_from_statefulset(hostpath, statefulset)
-
-        client.replace(statefulset)
-
-    def _unmount_hostpaths(self) -> bool:
-        client = Client()
-        hostpath_unmounted = False
-        statefulset = client.get(StatefulSet, self.charm.app.name, namespace=self.charm.model.name)
-
-        for hostpath in self.hostpaths:
-            if self._delete_hostpath_from_statefulset(hostpath, statefulset):
-                hostpath_unmounted = True
-
-        if hostpath_unmounted:
-            client.replace(statefulset)
-
-        return hostpath_unmounted
-
-    def _add_hostpath_to_statefulset(self, hostpath: HostPath, statefulset: StatefulSet):
-        # Add volume
-        logger.debug(f"adding volume {hostpath.config} to {self.charm.app.name} statefulset")
-        volume = Volume(
-            hostpath.config,
-            hostPath=HostPathVolumeSource(
-                path=self.charm.config[hostpath.config],
-                type="Directory",
-            ),
-        )
-        statefulset.spec.template.spec.volumes.append(volume)
-
-        # Add volumeMount
-        for statefulset_container in statefulset.spec.template.spec.containers:
-            if statefulset_container.name != self.container.name:
-                continue
-
-            logger.debug(
-                f"adding volumeMount {hostpath.config} to {self.container.name} container"
-            )
-            statefulset_container.volumeMounts.append(
-                VolumeMount(mountPath=hostpath.mount_path, name=hostpath.config)
-            )
-
-    def _delete_hostpath_from_statefulset(self, hostpath: HostPath, statefulset: StatefulSet):
-        hostpath_unmounted = False
-        for volume in statefulset.spec.template.spec.volumes:
-
-            if hostpath.config != volume.name:
-                continue
-
-            # Remove volumeMount
-            for statefulset_container in statefulset.spec.template.spec.containers:
-                if statefulset_container.name != self.container.name:
-                    continue
-                for volume_mount in statefulset_container.volumeMounts:
-                    if volume_mount.name != hostpath.config:
-                        continue
-
-                    logger.debug(
-                        f"removing volumeMount {hostpath.config} from {self.container.name} container"
-                    )
-                    statefulset_container.volumeMounts.remove(volume_mount)
-
-            # Remove volume
-            logger.debug(
-                f"removing volume {hostpath.config} from {self.charm.app.name} statefulset"
-            )
-            statefulset.spec.template.spec.volumes.remove(volume)
-
-            hostpath_unmounted = True
-        return hostpath_unmounted
-
-    def _get_vscode_command(
-        self,
-        pod_ip: str,
-        user: str = "root",
-        workspace_path: str = "/debug.code-workspace",
-    ) -> str:
-        return f"code --remote ssh-remote+{user}@{pod_ip} {workspace_path}"
-
-    def _restart(self):
-        self.container.exec(["kill", "-HUP", "1"])
diff --git a/installers/charm/osm-mon/lib/charms/osm_vca_integrator/v0/vca.py b/installers/charm/osm-mon/lib/charms/osm_vca_integrator/v0/vca.py
deleted file mode 100644 (file)
index 21dac69..0000000
+++ /dev/null
@@ -1,221 +0,0 @@
-# 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/osm-mon/metadata.yaml b/installers/charm/osm-mon/metadata.yaml
deleted file mode 100644 (file)
index 5bd1236..0000000
+++ /dev/null
@@ -1,70 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Overview on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
-
-name: osm-mon
-
-# The following metadata are human-readable and will be published prominently on Charmhub.
-
-display-name: OSM MON
-
-summary: OSM Monitoring Service (MON)
-
-description: |
-  A Kubernetes operator that deploys the Monitoring Service of OSM.
-
-  TODO: two sentences on MON
-
-  Small paragraph
-
-  This charm doesn't make sense on its own.
-  See more:
-    - https://charmhub.io/osm
-
-containers:
-  mon:
-    resource: mon-image
-
-# This file populates the Resources tab on Charmhub.
-
-resources:
-  mon-image:
-    type: oci-image
-    description: OCI image for mon
-    upstream-source: opensourcemano/mon
-
-requires:
-  kafka:
-    interface: kafka
-    limit: 1
-  mongodb:
-    interface: mongodb_client
-    limit: 1
-  keystone:
-    interface: keystone
-    limit: 1
-  prometheus:
-    interface: prometheus
-    limit: 1
-  vca:
-    interface: osm-vca
diff --git a/installers/charm/osm-mon/pyproject.toml b/installers/charm/osm-mon/pyproject.toml
deleted file mode 100644 (file)
index 16cf0f4..0000000
+++ /dev/null
@@ -1,52 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-
-# 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", "E501", "D107"]
-# D100, D101, D102, D103: Ignore missing docstrings in tests
-per-file-ignores = ["tests/*:D100,D101,D102,D103,D104"]
-docstring-convention = "google"
diff --git a/installers/charm/osm-mon/requirements.txt b/installers/charm/osm-mon/requirements.txt
deleted file mode 100644 (file)
index 398d4ad..0000000
+++ /dev/null
@@ -1,23 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-ops < 2.2
-lightkube
-lightkube-models
-# git+https://github.com/charmed-osm/config-validator/
diff --git a/installers/charm/osm-mon/src/charm.py b/installers/charm/osm-mon/src/charm.py
deleted file mode 100755 (executable)
index 12c5dcd..0000000
+++ /dev/null
@@ -1,300 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# Learn more at: https://juju.is/docs/sdk
-
-"""OSM MON charm.
-
-See more: https://charmhub.io/osm
-"""
-
-import logging
-from typing import Any, Dict
-
-from charms.data_platform_libs.v0.data_interfaces import DatabaseRequires
-from charms.kafka_k8s.v0.kafka import KafkaRequires, _KafkaAvailableEvent
-from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
-from charms.osm_libs.v0.utils import (
-    CharmError,
-    DebugMode,
-    HostPath,
-    check_container_ready,
-    check_service_active,
-)
-from charms.osm_vca_integrator.v0.vca import VcaDataChangedEvent, VcaRequires
-from lightkube.models.core_v1 import ServicePort
-from ops.charm import ActionEvent, CharmBase, CharmEvents
-from ops.framework import EventSource, StoredState
-from ops.main import main
-from ops.model import ActiveStatus, Container
-
-from legacy_interfaces import KeystoneClient, PrometheusClient
-
-HOSTPATHS = [
-    HostPath(
-        config="mon-hostpath",
-        container_path="/usr/lib/python3/dist-packages/osm_mon",
-    ),
-    HostPath(
-        config="common-hostpath",
-        container_path="/usr/lib/python3/dist-packages/osm_common",
-    ),
-    HostPath(
-        config="n2vc-hostpath",
-        container_path="/usr/lib/python3/dist-packages/n2vc",
-    ),
-]
-SERVICE_PORT = 8000
-
-logger = logging.getLogger(__name__)
-
-
-class MonEvents(CharmEvents):
-    """MON events."""
-
-    vca_data_changed = EventSource(VcaDataChangedEvent)
-    kafka_available = EventSource(_KafkaAvailableEvent)
-
-
-class OsmMonCharm(CharmBase):
-    """OSM MON Kubernetes sidecar charm."""
-
-    on = MonEvents()
-    _stored = StoredState()
-    container_name = "mon"
-    service_name = "mon"
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.kafka = KafkaRequires(self)
-        self.mongodb_client = DatabaseRequires(self, "mongodb", database_name="osm")
-        self.prometheus_client = PrometheusClient(self, "prometheus")
-        self.keystone_client = KeystoneClient(self, "keystone")
-        self.vca = VcaRequires(self)
-        self._observe_charm_events()
-        self.container: Container = self.unit.get_container(self.container_name)
-        self.debug_mode = DebugMode(self, self._stored, self.container, HOSTPATHS)
-        self._patch_k8s_service()
-
-    @property
-    def external_hostname(self) -> str:
-        """External hostname property.
-
-        Returns:
-            str: the external hostname from config.
-                If not set, return the ClusterIP service name.
-        """
-        return self.config.get("external-hostname") or self.app.name
-
-    # ---------------------------------------------------------------------------
-    #   Handlers for Charm Events
-    # ---------------------------------------------------------------------------
-
-    def _on_config_changed(self, _) -> None:
-        """Handler for the config-changed event."""
-        try:
-            self._validate_config()
-            self._check_relations()
-            # Check if the container is ready.
-            # Eventually it will become ready after the first pebble-ready event.
-            check_container_ready(self.container)
-            if not self.debug_mode.started:
-                self._configure_service(self.container)
-            # Update charm status
-            self._on_update_status()
-        except CharmError as e:
-            logger.debug(e.message)
-            self.unit.status = e.status
-
-    def _on_update_status(self, _=None) -> None:
-        """Handler for the update-status event."""
-        try:
-            self._validate_config()
-            self._check_relations()
-            check_container_ready(self.container)
-            if self.debug_mode.started:
-                return
-            check_service_active(self.container, self.service_name)
-            self.unit.status = ActiveStatus()
-        except CharmError as e:
-            logger.debug(e.message)
-            self.unit.status = e.status
-
-    def _on_required_relation_broken(self, _) -> None:
-        """Handler for the kafka-broken event."""
-        try:
-            check_container_ready(self.container)
-            check_service_active(self.container, self.service_name)
-            self.container.stop(self.container_name)
-        except CharmError:
-            pass
-        self._on_update_status()
-
-    def _on_get_debug_mode_information_action(self, event: ActionEvent) -> None:
-        """Handler for the get-debug-mode-information action event."""
-        if not self.debug_mode.started:
-            event.fail("debug-mode has not started. Hint: juju config mon debug-mode=true")
-            return
-
-        debug_info = {
-            "command": self.debug_mode.command,
-            "password": self.debug_mode.password,
-        }
-        event.set_results(debug_info)
-
-    # ---------------------------------------------------------------------------
-    #   Validation and configuration and more
-    # ---------------------------------------------------------------------------
-
-    def _observe_charm_events(self) -> None:
-        event_handler_mapping = {
-            # Core lifecycle events
-            self.on.mon_pebble_ready: self._on_config_changed,
-            self.on.config_changed: self._on_config_changed,
-            self.on.update_status: self._on_update_status,
-            # Relation events
-            self.on.vca_data_changed: self._on_config_changed,
-            self.on.kafka_available: self._on_config_changed,
-            self.on["kafka"].relation_broken: self._on_required_relation_broken,
-            self.mongodb_client.on.database_created: self._on_config_changed,
-            self.on["mongodb"].relation_broken: self._on_required_relation_broken,
-            # Action events
-            self.on.get_debug_mode_information_action: self._on_get_debug_mode_information_action,
-        }
-        for relation in [self.on[rel_name] for rel_name in ["prometheus", "keystone"]]:
-            event_handler_mapping[relation.relation_changed] = self._on_config_changed
-            event_handler_mapping[relation.relation_broken] = self._on_required_relation_broken
-
-        for event, handler in event_handler_mapping.items():
-            self.framework.observe(event, handler)
-
-    def _is_database_available(self) -> bool:
-        try:
-            return self.mongodb_client.is_resource_created()
-        except KeyError:
-            return False
-
-    def _validate_config(self) -> None:
-        """Validate charm configuration.
-
-        Raises:
-            CharmError: if charm configuration is invalid.
-        """
-        logger.debug("validating charm config")
-
-    def _check_relations(self) -> None:
-        """Validate charm relations.
-
-        Raises:
-            CharmError: if charm configuration is invalid.
-        """
-        logger.debug("check for missing relations")
-        missing_relations = []
-
-        if not self.kafka.host or not self.kafka.port:
-            missing_relations.append("kafka")
-        if not self._is_database_available():
-            missing_relations.append("mongodb")
-        if self.prometheus_client.is_missing_data_in_app():
-            missing_relations.append("prometheus")
-        if self.keystone_client.is_missing_data_in_app():
-            missing_relations.append("keystone")
-
-        if missing_relations:
-            relations_str = ", ".join(missing_relations)
-            one_relation_missing = len(missing_relations) == 1
-            error_msg = f'need {relations_str} relation{"" if one_relation_missing else "s"}'
-            logger.warning(error_msg)
-            raise CharmError(error_msg)
-
-    def _configure_service(self, container: Container) -> None:
-        """Add Pebble layer with the mon service."""
-        logger.debug(f"configuring {self.app.name} service")
-        container.add_layer("mon", self._get_layer(), combine=True)
-        container.replan()
-
-    def _get_layer(self) -> Dict[str, Any]:
-        """Get layer for Pebble."""
-        environment = {
-            # General configuration
-            "OSMMON_GLOBAL_LOGLEVEL": self.config["log-level"],
-            "OSMMON_OPENSTACK_DEFAULT_GRANULARITY": self.config["openstack-default-granularity"],
-            "OSMMON_GLOBAL_REQUEST_TIMEOUT": self.config["global-request-timeout"],
-            "OSMMON_COLLECTOR_INTERVAL": self.config["collector-interval"],
-            "OSMMON_EVALUATOR_INTERVAL": self.config["evaluator-interval"],
-            "OSMMON_COLLECTOR_VM_INFRA_METRICS": self.config["vm-infra-metrics"],
-            # Kafka configuration
-            "OSMMON_MESSAGE_DRIVER": "kafka",
-            "OSMMON_MESSAGE_HOST": self.kafka.host,
-            "OSMMON_MESSAGE_PORT": self.kafka.port,
-            # Database configuration
-            "OSMMON_DATABASE_DRIVER": "mongo",
-            "OSMMON_DATABASE_URI": self._get_mongodb_uri(),
-            "OSMMON_DATABASE_COMMONKEY": self.config["database-commonkey"],
-            # Prometheus/grafana configuration
-            "OSMMON_PROMETHEUS_URL": f"http://{self.prometheus_client.hostname}:{self.prometheus_client.port}",
-            "OSMMON_PROMETHEUS_USER": self.prometheus_client.user,
-            "OSMMON_PROMETHEUS_PASSWORD": self.prometheus_client.password,
-            "OSMMON_GRAFANA_URL": self.config["grafana-url"],
-            "OSMMON_GRAFANA_USER": self.config["grafana-user"],
-            "OSMMON_GRAFANA_PASSWORD": self.config["grafana-password"],
-            "OSMMON_KEYSTONE_ENABLED": self.config["keystone-enabled"],
-            "OSMMON_KEYSTONE_URL": self.keystone_client.host,
-            "OSMMON_KEYSTONE_DOMAIN_NAME": self.keystone_client.user_domain_name,
-            "OSMMON_KEYSTONE_SERVICE_PROJECT": self.keystone_client.service,
-            "OSMMON_KEYSTONE_SERVICE_USER": self.keystone_client.username,
-            "OSMMON_KEYSTONE_SERVICE_PASSWORD": self.keystone_client.password,
-            "OSMMON_KEYSTONE_SERVICE_PROJECT_DOMAIN_NAME": self.keystone_client.project_domain_name,
-        }
-        logger.info(f"{environment}")
-        if self.vca.data:
-            environment["OSMMON_VCA_HOST"] = self.vca.data.endpoints
-            environment["OSMMON_VCA_SECRET"] = self.vca.data.secret
-            environment["OSMMON_VCA_USER"] = self.vca.data.user
-            environment["OSMMON_VCA_CACERT"] = self.vca.data.cacert
-        return {
-            "summary": "mon layer",
-            "description": "pebble config layer for mon",
-            "services": {
-                self.service_name: {
-                    "override": "replace",
-                    "summary": "mon service",
-                    "command": "/bin/bash -c 'cd /app/osm_mon/ && /bin/bash start.sh'",
-                    "startup": "enabled",
-                    "user": "appuser",
-                    "group": "appuser",
-                    "working-dir": "/app/osm_mon",  # This parameter has no effect in Juju 2.9.x
-                    "environment": environment,
-                }
-            },
-        }
-
-    def _get_mongodb_uri(self):
-        return list(self.mongodb_client.fetch_relation_data().values())[0]["uris"]
-
-    def _patch_k8s_service(self) -> None:
-        port = ServicePort(SERVICE_PORT, name=f"{self.app.name}")
-        self.service_patcher = KubernetesServicePatch(self, [port])
-
-
-if __name__ == "__main__":  # pragma: no cover
-    main(OsmMonCharm)
diff --git a/installers/charm/osm-mon/src/legacy_interfaces.py b/installers/charm/osm-mon/src/legacy_interfaces.py
deleted file mode 100644 (file)
index 5deb3f5..0000000
+++ /dev/null
@@ -1,205 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-# flake8: noqa
-
-import ops
-
-
-class BaseRelationClient(ops.framework.Object):
-    """Requires side of a Kafka Endpoint"""
-
-    def __init__(
-        self,
-        charm: ops.charm.CharmBase,
-        relation_name: str,
-        mandatory_fields: list = [],
-    ):
-        super().__init__(charm, relation_name)
-        self.relation_name = relation_name
-        self.mandatory_fields = mandatory_fields
-        self._update_relation()
-
-    def get_data_from_unit(self, key: str):
-        if not self.relation:
-            # This update relation doesn't seem to be needed, but I added it because apparently
-            # the data is empty in the unit tests.
-            # In reality, the constructor is called in every hook.
-            # In the unit tests when doing an update_relation_data, apparently it is not called.
-            self._update_relation()
-        if self.relation:
-            for unit in self.relation.units:
-                data = self.relation.data[unit].get(key)
-                if data:
-                    return data
-
-    def get_data_from_app(self, key: str):
-        if not self.relation or self.relation.app not in self.relation.data:
-            # This update relation doesn't seem to be needed, but I added it because apparently
-            # the data is empty in the unit tests.
-            # In reality, the constructor is called in every hook.
-            # In the unit tests when doing an update_relation_data, apparently it is not called.
-            self._update_relation()
-        if self.relation and self.relation.app in self.relation.data:
-            data = self.relation.data[self.relation.app].get(key)
-            if data:
-                return data
-
-    def is_missing_data_in_unit(self):
-        return not all([self.get_data_from_unit(field) for field in self.mandatory_fields])
-
-    def is_missing_data_in_app(self):
-        return not all([self.get_data_from_app(field) for field in self.mandatory_fields])
-
-    def _update_relation(self):
-        self.relation = self.framework.model.get_relation(self.relation_name)
-
-
-class KeystoneClient(BaseRelationClient):
-    """Requires side of a Keystone Endpoint"""
-
-    mandatory_fields = [
-        "host",
-        "port",
-        "user_domain_name",
-        "project_domain_name",
-        "username",
-        "password",
-        "service",
-        "keystone_db_password",
-        "region_id",
-        "admin_username",
-        "admin_password",
-        "admin_project_name",
-    ]
-
-    def __init__(self, charm: ops.charm.CharmBase, relation_name: str):
-        super().__init__(charm, relation_name, self.mandatory_fields)
-
-    @property
-    def host(self):
-        return self.get_data_from_app("host")
-
-    @property
-    def port(self):
-        return self.get_data_from_app("port")
-
-    @property
-    def user_domain_name(self):
-        return self.get_data_from_app("user_domain_name")
-
-    @property
-    def project_domain_name(self):
-        return self.get_data_from_app("project_domain_name")
-
-    @property
-    def username(self):
-        return self.get_data_from_app("username")
-
-    @property
-    def password(self):
-        return self.get_data_from_app("password")
-
-    @property
-    def service(self):
-        return self.get_data_from_app("service")
-
-    @property
-    def keystone_db_password(self):
-        return self.get_data_from_app("keystone_db_password")
-
-    @property
-    def region_id(self):
-        return self.get_data_from_app("region_id")
-
-    @property
-    def admin_username(self):
-        return self.get_data_from_app("admin_username")
-
-    @property
-    def admin_password(self):
-        return self.get_data_from_app("admin_password")
-
-    @property
-    def admin_project_name(self):
-        return self.get_data_from_app("admin_project_name")
-
-
-class MongoClient(BaseRelationClient):
-    """Requires side of a Mongo Endpoint"""
-
-    mandatory_fields_mapping = {
-        "reactive": ["connection_string"],
-        "ops": ["replica_set_uri", "replica_set_name"],
-    }
-
-    def __init__(self, charm: ops.charm.CharmBase, relation_name: str):
-        super().__init__(charm, relation_name, mandatory_fields=[])
-
-    @property
-    def connection_string(self):
-        if self.is_opts():
-            replica_set_uri = self.get_data_from_unit("replica_set_uri")
-            replica_set_name = self.get_data_from_unit("replica_set_name")
-            return f"{replica_set_uri}?replicaSet={replica_set_name}"
-        else:
-            return self.get_data_from_unit("connection_string")
-
-    def is_opts(self):
-        return not self.is_missing_data_in_unit_ops()
-
-    def is_missing_data_in_unit(self):
-        return self.is_missing_data_in_unit_ops() and self.is_missing_data_in_unit_reactive()
-
-    def is_missing_data_in_unit_ops(self):
-        return not all(
-            [self.get_data_from_unit(field) for field in self.mandatory_fields_mapping["ops"]]
-        )
-
-    def is_missing_data_in_unit_reactive(self):
-        return not all(
-            [self.get_data_from_unit(field) for field in self.mandatory_fields_mapping["reactive"]]
-        )
-
-
-class PrometheusClient(BaseRelationClient):
-    """Requires side of a Prometheus Endpoint"""
-
-    mandatory_fields = ["hostname", "port"]
-
-    def __init__(self, charm: ops.charm.CharmBase, relation_name: str):
-        super().__init__(charm, relation_name, self.mandatory_fields)
-
-    @property
-    def hostname(self):
-        return self.get_data_from_app("hostname")
-
-    @property
-    def port(self):
-        return self.get_data_from_app("port")
-
-    @property
-    def user(self):
-        return self.get_data_from_app("user")
-
-    @property
-    def password(self):
-        return self.get_data_from_app("password")
diff --git a/installers/charm/osm-mon/tests/integration/test_charm.py b/installers/charm/osm-mon/tests/integration/test_charm.py
deleted file mode 100644 (file)
index caf8ded..0000000
+++ /dev/null
@@ -1,214 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-# Learn more about testing at: https://juju.is/docs/sdk/testing
-
-import asyncio
-import logging
-import shlex
-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())
-MON_APP = METADATA["name"]
-KAFKA_CHARM = "kafka-k8s"
-KAFKA_APP = "kafka"
-KEYSTONE_CHARM = "osm-keystone"
-KEYSTONE_APP = "keystone"
-MARIADB_CHARM = "charmed-osm-mariadb-k8s"
-MARIADB_APP = "mariadb"
-MONGO_DB_CHARM = "mongodb-k8s"
-MONGO_DB_APP = "mongodb"
-PROMETHEUS_CHARM = "osm-prometheus"
-PROMETHEUS_APP = "prometheus"
-ZOOKEEPER_CHARM = "zookeeper-k8s"
-ZOOKEEPER_APP = "zookeeper"
-VCA_CHARM = "osm-vca-integrator"
-VCA_APP = "vca"
-APPS = [KAFKA_APP, ZOOKEEPER_APP, KEYSTONE_APP, MONGO_DB_APP, MARIADB_APP, PROMETHEUS_APP, MON_APP]
-
-
-@pytest.mark.abort_on_fail
-async def test_mon_is_deployed(ops_test: OpsTest):
-    charm = await ops_test.build_charm(".")
-    resources = {"mon-image": METADATA["resources"]["mon-image"]["upstream-source"]}
-
-    await asyncio.gather(
-        ops_test.model.deploy(
-            charm, resources=resources, application_name=MON_APP, series="jammy"
-        ),
-        ops_test.model.deploy(KAFKA_CHARM, application_name=KAFKA_APP, channel="stable"),
-        ops_test.model.deploy(MONGO_DB_CHARM, application_name=MONGO_DB_APP, channel="5/edge"),
-        ops_test.model.deploy(MARIADB_CHARM, application_name=MARIADB_APP, channel="stable"),
-        ops_test.model.deploy(PROMETHEUS_CHARM, application_name=PROMETHEUS_APP, channel="stable"),
-        ops_test.model.deploy(ZOOKEEPER_CHARM, application_name=ZOOKEEPER_APP, channel="stable"),
-    )
-    keystone_image = "opensourcemano/keystone:testing-daily"
-    cmd = f"juju deploy {KEYSTONE_CHARM} {KEYSTONE_APP} --resource keystone-image={keystone_image} --channel=latest/beta --series jammy"
-    await ops_test.run(*shlex.split(cmd), check=True)
-
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS,
-        )
-    assert ops_test.model.applications[MON_APP].status == "blocked"
-    unit = ops_test.model.applications[MON_APP].units[0]
-    assert unit.workload_status_message == "need kafka, mongodb, prometheus, keystone relations"
-
-    logger.info("Adding relations for other components")
-    await ops_test.model.add_relation(KAFKA_APP, ZOOKEEPER_APP)
-    await ops_test.model.add_relation(MARIADB_APP, KEYSTONE_APP)
-
-    logger.info("Adding relations for MON")
-    await ops_test.model.add_relation(
-        "{}:mongodb".format(MON_APP), "{}:database".format(MONGO_DB_APP)
-    )
-    await ops_test.model.add_relation(MON_APP, KAFKA_APP)
-    await ops_test.model.add_relation(MON_APP, KEYSTONE_APP)
-    await ops_test.model.add_relation(MON_APP, PROMETHEUS_APP)
-
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS,
-            status="active",
-        )
-
-
-@pytest.mark.abort_on_fail
-async def test_mon_scales_up(ops_test: OpsTest):
-    logger.info("Scaling up osm-mon")
-    expected_units = 3
-    assert len(ops_test.model.applications[MON_APP].units) == 1
-    await ops_test.model.applications[MON_APP].scale(expected_units)
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=[MON_APP], status="active", wait_for_exact_units=expected_units
-        )
-
-
-@pytest.mark.abort_on_fail
-@pytest.mark.parametrize(
-    "relation_to_remove", [KAFKA_APP, MONGO_DB_APP, PROMETHEUS_APP, KEYSTONE_APP]
-)
-async def test_mon_blocks_without_relation(ops_test: OpsTest, relation_to_remove):
-    logger.info("Removing relation: %s", relation_to_remove)
-    # mongoDB relation is named "database"
-    local_relation = relation_to_remove
-    if relation_to_remove == MONGO_DB_APP:
-        local_relation = "database"
-    await asyncio.gather(
-        ops_test.model.applications[relation_to_remove].remove_relation(local_relation, MON_APP)
-    )
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(apps=[MON_APP])
-    assert ops_test.model.applications[MON_APP].status == "blocked"
-    for unit in ops_test.model.applications[MON_APP].units:
-        assert unit.workload_status_message == f"need {relation_to_remove} relation"
-    await ops_test.model.add_relation(MON_APP, relation_to_remove)
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS,
-            status="active",
-        )
-
-
-@pytest.mark.abort_on_fail
-async def test_mon_action_debug_mode_disabled(ops_test: OpsTest):
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS,
-            status="active",
-        )
-    logger.info("Running action 'get-debug-mode-information'")
-    action = (
-        await ops_test.model.applications[MON_APP]
-        .units[0]
-        .run_action("get-debug-mode-information")
-    )
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(apps=[MON_APP])
-    status = await ops_test.model.get_action_status(uuid_or_prefix=action.entity_id)
-    assert status[action.entity_id] == "failed"
-
-
-@pytest.mark.abort_on_fail
-async def test_mon_action_debug_mode_enabled(ops_test: OpsTest):
-    await ops_test.model.applications[MON_APP].set_config({"debug-mode": "true"})
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS,
-            status="active",
-        )
-    logger.info("Running action 'get-debug-mode-information'")
-    # list of units is not ordered
-    unit_id = list(
-        filter(
-            lambda x: (x.entity_id == f"{MON_APP}/0"), ops_test.model.applications[MON_APP].units
-        )
-    )[0]
-    action = await unit_id.run_action("get-debug-mode-information")
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(apps=[MON_APP])
-    status = await ops_test.model.get_action_status(uuid_or_prefix=action.entity_id)
-    message = await ops_test.model.get_action_output(action_uuid=action.entity_id)
-    assert status[action.entity_id] == "completed"
-    assert "command" in message
-    assert "password" in message
-
-
-@pytest.mark.abort_on_fail
-async def test_mon_integration_vca(ops_test: OpsTest):
-    await asyncio.gather(
-        ops_test.model.deploy(
-            VCA_CHARM, application_name=VCA_APP, channel="latest/beta", series="jammy"
-        ),
-    )
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=[VCA_APP],
-        )
-    controllers = (Path.home() / ".local/share/juju/controllers.yaml").read_text()
-    accounts = (Path.home() / ".local/share/juju/accounts.yaml").read_text()
-    public_key = (Path.home() / ".local/share/juju/ssh/juju_id_rsa.pub").read_text()
-    await ops_test.model.applications[VCA_APP].set_config(
-        {
-            "controllers": controllers,
-            "accounts": accounts,
-            "public-key": public_key,
-            "k8s-cloud": "microk8s",
-        }
-    )
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS + [VCA_APP],
-            status="active",
-        )
-    await ops_test.model.add_relation(MON_APP, VCA_APP)
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS + [VCA_APP],
-            status="active",
-        )
diff --git a/installers/charm/osm-mon/tests/unit/test_charm.py b/installers/charm/osm-mon/tests/unit/test_charm.py
deleted file mode 100644 (file)
index 33598fe..0000000
+++ /dev/null
@@ -1,114 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-# Learn more about testing at: https://juju.is/docs/sdk/testing
-
-import pytest
-from ops.model import ActiveStatus, BlockedStatus
-from ops.testing import Harness
-from pytest_mock import MockerFixture
-
-from charm import CharmError, OsmMonCharm, check_service_active
-
-container_name = "mon"
-service_name = "mon"
-
-
-@pytest.fixture
-def harness(mocker: MockerFixture):
-    mocker.patch("charm.KubernetesServicePatch", lambda x, y: None)
-    harness = Harness(OsmMonCharm)
-    harness.begin()
-    harness.container_pebble_ready(container_name)
-    yield harness
-    harness.cleanup()
-
-
-def test_missing_relations(harness: Harness):
-    harness.charm.on.config_changed.emit()
-    assert type(harness.charm.unit.status) == BlockedStatus
-    assert all(
-        relation in harness.charm.unit.status.message
-        for relation in ["mongodb", "kafka", "prometheus", "keystone"]
-    )
-
-
-def test_ready(harness: Harness):
-    _add_relations(harness)
-    assert harness.charm.unit.status == ActiveStatus()
-
-
-def test_container_stops_after_relation_broken(harness: Harness):
-    harness.charm.on[container_name].pebble_ready.emit(container_name)
-    container = harness.charm.unit.get_container(container_name)
-    relation_ids = _add_relations(harness)
-    check_service_active(container, service_name)
-    harness.remove_relation(relation_ids[0])
-    with pytest.raises(CharmError):
-        check_service_active(container, service_name)
-
-
-def _add_relations(harness: Harness):
-    relation_ids = []
-    # Add mongo relation
-    relation_id = harness.add_relation("mongodb", "mongodb")
-    harness.add_relation_unit(relation_id, "mongodb/0")
-    harness.update_relation_data(
-        relation_id,
-        "mongodb",
-        {"uris": "mongodb://:1234", "username": "user", "password": "password"},
-    )
-    relation_ids.append(relation_id)
-    # Add kafka relation
-    relation_id = harness.add_relation("kafka", "kafka")
-    harness.add_relation_unit(relation_id, "kafka/0")
-    harness.update_relation_data(relation_id, "kafka", {"host": "kafka", "port": "9092"})
-    relation_ids.append(relation_id)
-    # Add prometheus relation
-    relation_id = harness.add_relation("prometheus", "prometheus")
-    harness.add_relation_unit(relation_id, "prometheus/0")
-    harness.update_relation_data(
-        relation_id, "prometheus", {"hostname": "prometheus", "port": "9090"}
-    )
-    relation_ids.append(relation_id)
-    # Add keystone relation
-    relation_id = harness.add_relation("keystone", "keystone")
-    harness.add_relation_unit(relation_id, "keystone/0")
-    harness.update_relation_data(
-        relation_id,
-        "keystone",
-        {
-            "host": "host",
-            "port": "port",
-            "user_domain_name": "user_domain_name",
-            "project_domain_name": "project_domain_name",
-            "username": "username",
-            "password": "password",
-            "service": "service",
-            "keystone_db_password": "keystone_db_password",
-            "region_id": "region_id",
-            "admin_username": "admin_username",
-            "admin_password": "admin_password",
-            "admin_project_name": "admin_project_name",
-        },
-    )
-    relation_ids.append(relation_id)
-    return relation_ids
diff --git a/installers/charm/osm-mon/tox.ini b/installers/charm/osm-mon/tox.ini
deleted file mode 100644 (file)
index 64bab10..0000000
+++ /dev/null
@@ -1,92 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-
-[tox]
-skipsdist=True
-skip_missing_interpreters = True
-envlist = lint, unit, integration
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3.8
-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-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    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
-    # 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} \
-        -m pytest --ignore={[vars]tst_path}integration -v --tb native -s {posargs}
-    coverage report
-    coverage xml
-
-[testenv:integration]
-description = Run integration tests
-deps =
-    pytest
-    juju<3
-    pytest-operator
-    -r{toxinidir}/requirements.txt
-commands =
-    pytest -v --tb native --ignore={[vars]tst_path}unit --log-cli-level=INFO -s {posargs} --cloud microk8s
diff --git a/installers/charm/osm-nbi/.gitignore b/installers/charm/osm-nbi/.gitignore
deleted file mode 100644 (file)
index 87d0a58..0000000
+++ /dev/null
@@ -1,29 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-venv/
-build/
-*.charm
-.tox/
-.coverage
-coverage.xml
-__pycache__/
-*.py[cod]
-.vscode
\ No newline at end of file
diff --git a/installers/charm/osm-nbi/.jujuignore b/installers/charm/osm-nbi/.jujuignore
deleted file mode 100644 (file)
index 17c7a8b..0000000
+++ /dev/null
@@ -1,23 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-/venv
-*.py[cod]
-*.charm
diff --git a/installers/charm/osm-nbi/CONTRIBUTING.md b/installers/charm/osm-nbi/CONTRIBUTING.md
deleted file mode 100644 (file)
index c59b970..0000000
+++ /dev/null
@@ -1,78 +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.
-
-For those usages not covered by the Apache License, Version 2.0 please
-contact: legal@canonical.com
-
-To get in touch with the maintainers, please contact:
-osm-charmers@lists.launchpad.net -->
-
-# Contributing
-
-## Overview
-
-This documents explains the processes and practices recommended for contributing enhancements to
-this operator.
-
-- Generally, before developing enhancements to this charm, you should consider [opening an issue
-  ](https://osm.etsi.org/bugzilla/enter_bug.cgi?product=OSM) explaining your use case. (Component=devops, version=master)
-- If you would like to chat with us about your use-cases or proposed implementation, you can reach
-  us at [OSM Juju public channel](https://opensourcemano.slack.com/archives/C027KJGPECA).
-- 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 gerrit patch onto
-  the `master` branch.
-
-## 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 dev
-# Enable DEBUG logging
-juju model-config logging-config="<root>=INFO;unit=DEBUG"
-# Deploy the charm
-juju deploy ./osm-nbi_ubuntu-22.04-amd64.charm \
-    --resource nbi-image=opensourcemano/nbi:testing-daily --series jammy
-```
diff --git a/installers/charm/osm-nbi/LICENSE b/installers/charm/osm-nbi/LICENSE
deleted file mode 100644 (file)
index 7e9d504..0000000
+++ /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 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.
diff --git a/installers/charm/osm-nbi/README.md b/installers/charm/osm-nbi/README.md
deleted file mode 100644 (file)
index 5cff9bf..0000000
+++ /dev/null
@@ -1,43 +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.
-
-For those usages not covered by the Apache License, Version 2.0 please
-contact: legal@canonical.com
-
-To get in touch with the maintainers, please contact:
-osm-charmers@lists.launchpad.net -->
-
-<!-- 
-Avoid using this README file for information that is maintained or published elsewhere, e.g.:
-
-* metadata.yaml > published on Charmhub
-* documentation > published on (or linked to from) Charmhub
-* detailed contribution guide > documentation or CONTRIBUTING.md
-
-Use links instead. 
--->
-
-# OSM NBI
-
-Charmhub package name: osm-nbi
-More information: https://charmhub.io/osm-nbi
-
-## Other resources
-
-* [Read more](https://osm.etsi.org/docs/user-guide/latest/) 
-
-* [Contributing](https://osm.etsi.org/gitweb/?p=osm/devops.git;a=blob;f=installers/charm/osm-nbi/CONTRIBUTING.md)
-
-* See the [Juju SDK documentation](https://juju.is/docs/sdk) for more information about developing and improving charms.
-                                                           
diff --git a/installers/charm/osm-nbi/actions.yaml b/installers/charm/osm-nbi/actions.yaml
deleted file mode 100644 (file)
index 0d73468..0000000
+++ /dev/null
@@ -1,26 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Actions tab on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
-
-get-debug-mode-information:
-  description: Get information to debug the container
diff --git a/installers/charm/osm-nbi/charmcraft.yaml b/installers/charm/osm-nbi/charmcraft.yaml
deleted file mode 100644 (file)
index 3fce6d0..0000000
+++ /dev/null
@@ -1,36 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-
-type: charm
-bases:
-  - build-on:
-      - name: "ubuntu"
-        channel: "22.04"
-    run-on:
-      - name: "ubuntu"
-        channel: "22.04"
-
-parts:
-  charm:
-    build-packages:
-      - git
-    prime:
-      - files/*
diff --git a/installers/charm/osm-nbi/config.yaml b/installers/charm/osm-nbi/config.yaml
deleted file mode 100644 (file)
index d2c8c62..0000000
+++ /dev/null
@@ -1,109 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Configure tab on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
-
-options:
-  log-level:
-    default: "INFO"
-    description: |
-      Set the Logging Level.
-
-      Options:
-        - TRACE
-        - DEBUG
-        - INFO
-        - WARN
-        - ERROR
-        - FATAL
-    type: string
-  database-commonkey:
-    description: Database COMMON KEY
-    type: string
-    default: osm
-
-  # Ingress options
-  external-hostname:
-    default: ""
-    description: |
-      The url that will be configured in the Kubernetes ingress.
-
-      The easiest way of configuring the external-hostname without having the DNS setup is by using
-      a Wildcard DNS like nip.io constructing the url like so:
-        - nbi.127.0.0.1.nip.io (valid within the K8s cluster node)
-        - nbi.<k8s-worker-ip>.nip.io (valid from outside the K8s cluster node)
-
-      This option is only applicable when the Kubernetes cluster has nginx ingress configured
-      and the charm is related to the nginx-ingress-integrator.
-      See more: https://charmhub.io/nginx-ingress-integrator
-    type: string
-  max-body-size:
-    default: 20
-    description: Max allowed body-size (for file uploads) in megabytes, set to 0 to
-      disable limits.
-    type: int
-  tls-secret-name:
-    description: TLS secret name to use for ingress.
-    type: string
-
-  # Debug-mode options
-  debug-mode:
-    type: boolean
-    description: |
-      Great for OSM Developers! (Not recommended for production deployments)
-
-      This action activates the Debug Mode, which sets up the container to be ready for debugging.
-      As part of the setup, SSH is enabled and a VSCode workspace file is automatically populated.
-
-      After enabling the debug-mode, execute the following command to get the information you need
-      to start debugging:
-        `juju run-action <unit name> get-debug-mode-information --wait`
-
-      The previous command returns the command you need to execute, and the SSH password that was set.
-
-      See also:
-        - https://charmhub.io/osm-nbi/configure#nbi-hostpath
-        - https://charmhub.io/osm-nbi/configure#common-hostpath
-    default: false
-  nbi-hostpath:
-    type: string
-    description: |
-      Set this config to the local path of the NBI module to persist the changes done during the
-      debug-mode session.
-
-      Example:
-        $ git clone "https://osm.etsi.org/gerrit/osm/NBI" /home/ubuntu/NBI
-        $ juju config nbi nbi-hostpath=/home/ubuntu/NBI
-
-      This configuration only applies if option `debug-mode` is set to true.
-
-  common-hostpath:
-    type: string
-    description: |
-      Set this config to the local path of the common module to persist the changes done during the
-      debug-mode session.
-
-      Example:
-        $ git clone "https://osm.etsi.org/gerrit/osm/common" /home/ubuntu/common
-        $ juju config nbi common-hostpath=/home/ubuntu/common
-
-      This configuration only applies if option `debug-mode` is set to true.
diff --git a/installers/charm/osm-nbi/files/vscode-workspace.json b/installers/charm/osm-nbi/files/vscode-workspace.json
deleted file mode 100644 (file)
index f2baa1d..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-{
-    "folders": [
-        {
-            "path": "/usr/lib/python3/dist-packages/osm_nbi"
-        },
-        {
-            "path": "/usr/lib/python3/dist-packages/osm_common"
-        },
-        {
-            "path": "/usr/lib/python3/dist-packages/osm_im"
-        },
-    ],
-    "settings": {},
-    "launch": {
-        "version": "0.2.0",
-        "configurations": [
-            {
-                "name": "NBI",
-                "type": "python",
-                "request": "launch",
-                "module": "osm_nbi.nbi",
-                "justMyCode": false,
-            }
-        ]
-    }
-}
\ No newline at end of file
diff --git a/installers/charm/osm-nbi/lib/charms/data_platform_libs/v0/data_interfaces.py b/installers/charm/osm-nbi/lib/charms/data_platform_libs/v0/data_interfaces.py
deleted file mode 100644 (file)
index b3da5aa..0000000
+++ /dev/null
@@ -1,1130 +0,0 @@
-# Copyright 2023 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.
-
-"""Library to manage the relation for the data-platform products.
-
-This library contains the Requires and Provides classes for handling the relation
-between an application and multiple managed application supported by the data-team:
-MySQL, Postgresql, MongoDB, Redis,  and Kakfa.
-
-### Database (MySQL, Postgresql, MongoDB, and Redis)
-
-#### Requires Charm
-This library is a uniform interface to a selection of common database
-metadata, with added custom events that add convenience to database management,
-and methods to consume the application related data.
-
-
-Following an example of using the DatabaseCreatedEvent, in the context of the
-application charm code:
-
-```python
-
-from charms.data_platform_libs.v0.data_interfaces import (
-    DatabaseCreatedEvent,
-    DatabaseRequires,
-)
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Charm events defined in the database requires charm library.
-        self.database = DatabaseRequires(self, relation_name="database", database_name="database")
-        self.framework.observe(self.database.on.database_created, self._on_database_created)
-
-    def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-
-        # Start application with rendered configuration
-        self._start_application(config_file)
-
-        # Set active status
-        self.unit.status = ActiveStatus("received database credentials")
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
--  database_created: event emitted when the requested database is created.
--  endpoints_changed: event emitted when the read/write endpoints of the database have changed.
--  read_only_endpoints_changed: event emitted when the read-only endpoints of the database
-  have changed. Event is not triggered if read/write endpoints changed too.
-
-If it is needed to connect multiple database clusters to the same relation endpoint
-the application charm can implement the same code as if it would connect to only
-one database cluster (like the above code example).
-
-To differentiate multiple clusters connected to the same relation endpoint
-the application charm can use the name of the remote application:
-
-```python
-
-def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-    # Get the remote app name of the cluster that triggered this event
-    cluster = event.relation.app.name
-```
-
-It is also possible to provide an alias for each different database cluster/relation.
-
-So, it is possible to differentiate the clusters in two ways.
-The first is to use the remote application name, i.e., `event.relation.app.name`, as above.
-
-The second way is to use different event handlers to handle each cluster events.
-The implementation would be something like the following code:
-
-```python
-
-from charms.data_platform_libs.v0.data_interfaces import (
-    DatabaseCreatedEvent,
-    DatabaseRequires,
-)
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Define the cluster aliases and one handler for each cluster database created event.
-        self.database = DatabaseRequires(
-            self,
-            relation_name="database",
-            database_name="database",
-            relations_aliases = ["cluster1", "cluster2"],
-        )
-        self.framework.observe(
-            self.database.on.cluster1_database_created, self._on_cluster1_database_created
-        )
-        self.framework.observe(
-            self.database.on.cluster2_database_created, self._on_cluster2_database_created
-        )
-
-    def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster1
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-    def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster2
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-```
-
-### Provider Charm
-
-Following an example of using the DatabaseRequestedEvent, in the context of the
-database charm code:
-
-```python
-from charms.data_platform_libs.v0.data_interfaces import DatabaseProvides
-
-class SampleCharm(CharmBase):
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        # Charm events defined in the database provides charm library.
-        self.provided_database = DatabaseProvides(self, relation_name="database")
-        self.framework.observe(self.provided_database.on.database_requested,
-            self._on_database_requested)
-        # Database generic helper
-        self.database = DatabaseHelper()
-
-    def _on_database_requested(self, event: DatabaseRequestedEvent) -> None:
-        # Handle the event triggered by a new database requested in the relation
-        # Retrieve the database name using the charm library.
-        db_name = event.database
-        # generate a new user credential
-        username = self.database.generate_user()
-        password = self.database.generate_password()
-        # set the credentials for the relation
-        self.provided_database.set_credentials(event.relation.id, username, password)
-        # set other variables for the relation event.set_tls("False")
-```
-As shown above, the library provides a custom event (database_requested) to handle
-the situation when an application charm requests a new database to be created.
-It's preferred to subscribe to this event instead of relation changed event to avoid
-creating a new database when other information other than a database name is
-exchanged in the relation databag.
-
-### Kafka
-
-This library is the interface to use and interact with the Kafka charm. This library contains
-custom events that add convenience to manage Kafka, and provides methods to consume the
-application related data.
-
-#### Requirer Charm
-
-```python
-
-from charms.data_platform_libs.v0.data_interfaces import (
-    BootstrapServerChangedEvent,
-    KafkaRequires,
-    TopicCreatedEvent,
-)
-
-class ApplicationCharm(CharmBase):
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.kafka = KafkaRequires(self, "kafka_client", "test-topic")
-        self.framework.observe(
-            self.kafka.on.bootstrap_server_changed, self._on_kafka_bootstrap_server_changed
-        )
-        self.framework.observe(
-            self.kafka.on.topic_created, self._on_kafka_topic_created
-        )
-
-    def _on_kafka_bootstrap_server_changed(self, event: BootstrapServerChangedEvent):
-        # Event triggered when a bootstrap server was changed for this application
-
-        new_bootstrap_server = event.bootstrap_server
-        ...
-
-    def _on_kafka_topic_created(self, event: TopicCreatedEvent):
-        # Event triggered when a topic was created for this application
-        username = event.username
-        password = event.password
-        tls = event.tls
-        tls_ca= event.tls_ca
-        bootstrap_server event.bootstrap_server
-        consumer_group_prefic = event.consumer_group_prefix
-        zookeeper_uris = event.zookeeper_uris
-        ...
-
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
-- topic_created: event emitted when the requested topic is created.
-- bootstrap_server_changed: event emitted when the bootstrap server have changed.
-- credential_changed: event emitted when the credentials of Kafka changed.
-
-### Provider Charm
-
-Following the previous example, this is an example of the provider charm.
-
-```python
-class SampleCharm(CharmBase):
-
-from charms.data_platform_libs.v0.data_interfaces import (
-    KafkaProvides,
-    TopicRequestedEvent,
-)
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Default charm events.
-        self.framework.observe(self.on.start, self._on_start)
-
-        # Charm events defined in the Kafka Provides charm library.
-        self.kafka_provider = KafkaProvides(self, relation_name="kafka_client")
-        self.framework.observe(self.kafka_provider.on.topic_requested, self._on_topic_requested)
-        # Kafka generic helper
-        self.kafka = KafkaHelper()
-
-    def _on_topic_requested(self, event: TopicRequestedEvent):
-        # Handle the on_topic_requested event.
-
-        topic = event.topic
-        relation_id = event.relation.id
-        # set connection info in the databag relation
-        self.kafka_provider.set_bootstrap_server(relation_id, self.kafka.get_bootstrap_server())
-        self.kafka_provider.set_credentials(relation_id, username=username, password=password)
-        self.kafka_provider.set_consumer_group_prefix(relation_id, ...)
-        self.kafka_provider.set_tls(relation_id, "False")
-        self.kafka_provider.set_zookeeper_uris(relation_id, ...)
-
-```
-As shown above, the library provides a custom event (topic_requested) to handle
-the situation when an application charm requests a new topic to be created.
-It is preferred to subscribe to this event instead of relation changed event to avoid
-creating a new topic when other information other than a topic name is
-exchanged in the relation databag.
-"""
-
-import json
-import logging
-from abc import ABC, abstractmethod
-from collections import namedtuple
-from datetime import datetime
-from typing import List, Optional
-
-from ops.charm import (
-    CharmBase,
-    CharmEvents,
-    RelationChangedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-)
-from ops.framework import EventSource, Object
-from ops.model import Relation
-
-# The unique Charmhub library identifier, never change it
-LIBID = "6c3e6b6680d64e9c89e611d1a15f65be"
-
-# 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 = 7
-
-PYDEPS = ["ops>=2.0.0"]
-
-logger = logging.getLogger(__name__)
-
-Diff = namedtuple("Diff", "added changed deleted")
-Diff.__doc__ = """
-A tuple for storing the diff between two data mappings.
-
-added - keys that were added
-changed - keys that still exist but have new values
-deleted - key that were deleted"""
-
-
-def diff(event: RelationChangedEvent, bucket: str) -> Diff:
-    """Retrieves the diff of the data in the relation changed databag.
-
-    Args:
-        event: relation changed event.
-        bucket: bucket of the databag (app or unit)
-
-    Returns:
-        a Diff instance containing the added, deleted and changed
-            keys from the event relation databag.
-    """
-    # Retrieve the old data from the data key in the application relation databag.
-    old_data = json.loads(event.relation.data[bucket].get("data", "{}"))
-    # Retrieve the new data from the event relation databag.
-    new_data = {
-        key: value for key, value in event.relation.data[event.app].items() if key != "data"
-    }
-
-    # These are the keys that were added to the databag and triggered this event.
-    added = new_data.keys() - old_data.keys()
-    # These are the keys that were removed from the databag and triggered this event.
-    deleted = old_data.keys() - new_data.keys()
-    # These are the keys that already existed in the databag,
-    # but had their values changed.
-    changed = {key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]}
-    # Convert the new_data to a serializable format and save it for a next diff check.
-    event.relation.data[bucket].update({"data": json.dumps(new_data)})
-
-    # Return the diff with all possible changes.
-    return Diff(added, changed, deleted)
-
-
-# Base DataProvides and DataRequires
-
-
-class DataProvides(Object, ABC):
-    """Base provides-side of the data products relation."""
-
-    def __init__(self, charm: CharmBase, relation_name: str) -> None:
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.framework.observe(
-            charm.on[relation_name].relation_changed,
-            self._on_relation_changed,
-        )
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        return diff(event, self.local_app)
-
-    @abstractmethod
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the relation data has changed."""
-        raise NotImplementedError
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation id).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = {
-                key: value for key, value in relation.data[relation.app].items() if key != "data"
-            }
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return list(self.charm.model.relations[self.relation_name])
-
-    def set_credentials(self, relation_id: int, username: str, password: str) -> None:
-        """Set credentials.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            username: user that was created.
-            password: password of the created user.
-        """
-        self._update_relation_data(
-            relation_id,
-            {
-                "username": username,
-                "password": password,
-            },
-        )
-
-    def set_tls(self, relation_id: int, tls: str) -> None:
-        """Set whether TLS is enabled.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            tls: whether tls is enabled (True or False).
-        """
-        self._update_relation_data(relation_id, {"tls": tls})
-
-    def set_tls_ca(self, relation_id: int, tls_ca: str) -> None:
-        """Set the TLS CA in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            tls_ca: TLS certification authority.
-        """
-        self._update_relation_data(relation_id, {"tls_ca": tls_ca})
-
-
-class DataRequires(Object, ABC):
-    """Requires-side of the relation."""
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        extra_user_roles: str = None,
-    ):
-        """Manager of base client relations."""
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.extra_user_roles = extra_user_roles
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined, self._on_relation_joined_event
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed, self._on_relation_changed_event
-        )
-
-    @abstractmethod
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the relation."""
-        raise NotImplementedError
-
-    @abstractmethod
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        raise NotImplementedError
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-        Function cannot be used in `*-relation-broken` events and will raise an exception.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation ID).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = {
-                key: value for key, value in relation.data[relation.app].items() if key != "data"
-            }
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        return diff(event, self.local_unit)
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return [
-            relation
-            for relation in self.charm.model.relations[self.relation_name]
-            if self._is_relation_active(relation)
-        ]
-
-    @staticmethod
-    def _is_relation_active(relation: Relation):
-        try:
-            _ = repr(relation.data)
-            return True
-        except RuntimeError:
-            return False
-
-    @staticmethod
-    def _is_resource_created_for_relation(relation: Relation):
-        return (
-            "username" in relation.data[relation.app] and "password" in relation.data[relation.app]
-        )
-
-    def is_resource_created(self, relation_id: Optional[int] = None) -> bool:
-        """Check if the resource has been created.
-
-        This function can be used to check if the Provider answered with data in the charm code
-        when outside an event callback.
-
-        Args:
-            relation_id (int, optional): When provided the check is done only for the relation id
-                provided, otherwise the check is done for all relations
-
-        Returns:
-            True or False
-
-        Raises:
-            IndexError: If relation_id is provided but that relation does not exist
-        """
-        if relation_id is not None:
-            try:
-                relation = [relation for relation in self.relations if relation.id == relation_id][
-                    0
-                ]
-                return self._is_resource_created_for_relation(relation)
-            except IndexError:
-                raise IndexError(f"relation id {relation_id} cannot be accessed")
-        else:
-            return (
-                all(
-                    [
-                        self._is_resource_created_for_relation(relation)
-                        for relation in self.relations
-                    ]
-                )
-                if self.relations
-                else False
-            )
-
-
-# General events
-
-
-class ExtraRoleEvent(RelationEvent):
-    """Base class for data events."""
-
-    @property
-    def extra_user_roles(self) -> Optional[str]:
-        """Returns the extra user roles that were requested."""
-        return self.relation.data[self.relation.app].get("extra-user-roles")
-
-
-class AuthenticationEvent(RelationEvent):
-    """Base class for authentication fields for events."""
-
-    @property
-    def username(self) -> Optional[str]:
-        """Returns the created username."""
-        return self.relation.data[self.relation.app].get("username")
-
-    @property
-    def password(self) -> Optional[str]:
-        """Returns the password for the created user."""
-        return self.relation.data[self.relation.app].get("password")
-
-    @property
-    def tls(self) -> Optional[str]:
-        """Returns whether TLS is configured."""
-        return self.relation.data[self.relation.app].get("tls")
-
-    @property
-    def tls_ca(self) -> Optional[str]:
-        """Returns TLS CA."""
-        return self.relation.data[self.relation.app].get("tls-ca")
-
-
-# Database related events and fields
-
-
-class DatabaseProvidesEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def database(self) -> Optional[str]:
-        """Returns the database that was requested."""
-        return self.relation.data[self.relation.app].get("database")
-
-
-class DatabaseRequestedEvent(DatabaseProvidesEvent, ExtraRoleEvent):
-    """Event emitted when a new database is requested for use on this relation."""
-
-
-class DatabaseProvidesEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_requested = EventSource(DatabaseRequestedEvent)
-
-
-class DatabaseRequiresEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read/write endpoints."""
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def read_only_endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read only endpoints."""
-        return self.relation.data[self.relation.app].get("read-only-endpoints")
-
-    @property
-    def replset(self) -> Optional[str]:
-        """Returns the replicaset name.
-
-        MongoDB only.
-        """
-        return self.relation.data[self.relation.app].get("replset")
-
-    @property
-    def uris(self) -> Optional[str]:
-        """Returns the connection URIs.
-
-        MongoDB, Redis, OpenSearch.
-        """
-        return self.relation.data[self.relation.app].get("uris")
-
-    @property
-    def version(self) -> Optional[str]:
-        """Returns the version of the database.
-
-        Version as informed by the database daemon.
-        """
-        return self.relation.data[self.relation.app].get("version")
-
-
-class DatabaseCreatedEvent(AuthenticationEvent, DatabaseRequiresEvent):
-    """Event emitted when a new database is created for use on this relation."""
-
-
-class DatabaseEndpointsChangedEvent(AuthenticationEvent, DatabaseRequiresEvent):
-    """Event emitted when the read/write endpoints are changed."""
-
-
-class DatabaseReadOnlyEndpointsChangedEvent(AuthenticationEvent, DatabaseRequiresEvent):
-    """Event emitted when the read only endpoints are changed."""
-
-
-class DatabaseRequiresEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_created = EventSource(DatabaseCreatedEvent)
-    endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
-    read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent)
-
-
-# Database Provider and Requires
-
-
-class DatabaseProvides(DataProvides):
-    """Provider-side of the database relations."""
-
-    on = DatabaseProvidesEvents()
-
-    def __init__(self, charm: CharmBase, relation_name: str) -> None:
-        super().__init__(charm, relation_name)
-
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the relation has changed."""
-        # Only the leader should handle this event.
-        if not self.local_unit.is_leader():
-            return
-
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Emit a database requested event if the setup key (database name and optional
-        # extra user roles) was added to the relation databag by the application.
-        if "database" in diff.added:
-            self.on.database_requested.emit(event.relation, app=event.app, unit=event.unit)
-
-    def set_endpoints(self, relation_id: int, connection_strings: str) -> None:
-        """Set database primary connections.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            connection_strings: database hosts and ports comma separated list.
-        """
-        self._update_relation_data(relation_id, {"endpoints": connection_strings})
-
-    def set_read_only_endpoints(self, relation_id: int, connection_strings: str) -> None:
-        """Set database replicas connection strings.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            connection_strings: database hosts and ports comma separated list.
-        """
-        self._update_relation_data(relation_id, {"read-only-endpoints": connection_strings})
-
-    def set_replset(self, relation_id: int, replset: str) -> None:
-        """Set replica set name in the application relation databag.
-
-        MongoDB only.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            replset: replica set name.
-        """
-        self._update_relation_data(relation_id, {"replset": replset})
-
-    def set_uris(self, relation_id: int, uris: str) -> None:
-        """Set the database connection URIs in the application relation databag.
-
-        MongoDB, Redis, and OpenSearch only.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            uris: connection URIs.
-        """
-        self._update_relation_data(relation_id, {"uris": uris})
-
-    def set_version(self, relation_id: int, version: str) -> None:
-        """Set the database version in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            version: database version.
-        """
-        self._update_relation_data(relation_id, {"version": version})
-
-
-class DatabaseRequires(DataRequires):
-    """Requires-side of the database relation."""
-
-    on = DatabaseRequiresEvents()
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        database_name: str,
-        extra_user_roles: str = None,
-        relations_aliases: List[str] = None,
-    ):
-        """Manager of database client relations."""
-        super().__init__(charm, relation_name, extra_user_roles)
-        self.database = database_name
-        self.relations_aliases = relations_aliases
-
-        # Define custom event names for each alias.
-        if relations_aliases:
-            # Ensure the number of aliases does not exceed the maximum
-            # of connections allowed in the specific relation.
-            relation_connection_limit = self.charm.meta.requires[relation_name].limit
-            if len(relations_aliases) != relation_connection_limit:
-                raise ValueError(
-                    f"The number of aliases must match the maximum number of connections allowed in the relation. "
-                    f"Expected {relation_connection_limit}, got {len(relations_aliases)}"
-                )
-
-            for relation_alias in relations_aliases:
-                self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent)
-                self.on.define_event(
-                    f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent
-                )
-                self.on.define_event(
-                    f"{relation_alias}_read_only_endpoints_changed",
-                    DatabaseReadOnlyEndpointsChangedEvent,
-                )
-
-    def _assign_relation_alias(self, relation_id: int) -> None:
-        """Assigns an alias to a relation.
-
-        This function writes in the unit data bag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-        """
-        # If no aliases were provided, return immediately.
-        if not self.relations_aliases:
-            return
-
-        # Return if an alias was already assigned to this relation
-        # (like when there are more than one unit joining the relation).
-        if (
-            self.charm.model.get_relation(self.relation_name, relation_id)
-            .data[self.local_unit]
-            .get("alias")
-        ):
-            return
-
-        # Retrieve the available aliases (the ones that weren't assigned to any relation).
-        available_aliases = self.relations_aliases[:]
-        for relation in self.charm.model.relations[self.relation_name]:
-            alias = relation.data[self.local_unit].get("alias")
-            if alias:
-                logger.debug("Alias %s was already assigned to relation %d", alias, relation.id)
-                available_aliases.remove(alias)
-
-        # Set the alias in the unit relation databag of the specific relation.
-        relation = self.charm.model.get_relation(self.relation_name, relation_id)
-        relation.data[self.local_unit].update({"alias": available_aliases[0]})
-
-    def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None:
-        """Emit an aliased event to a particular relation if it has an alias.
-
-        Args:
-            event: the relation changed event that was received.
-            event_name: the name of the event to emit.
-        """
-        alias = self._get_relation_alias(event.relation.id)
-        if alias:
-            getattr(self.on, f"{alias}_{event_name}").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-    def _get_relation_alias(self, relation_id: int) -> Optional[str]:
-        """Returns the relation alias.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-
-        Returns:
-            the relation alias or None if the relation was not found.
-        """
-        for relation in self.charm.model.relations[self.relation_name]:
-            if relation.id == relation_id:
-                return relation.data[self.local_unit].get("alias")
-        return None
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the database relation."""
-        # If relations aliases were provided, assign one to the relation.
-        self._assign_relation_alias(event.relation.id)
-
-        # Sets both database and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the database.
-        if self.extra_user_roles:
-            self._update_relation_data(
-                event.relation.id,
-                {
-                    "database": self.database,
-                    "extra-user-roles": self.extra_user_roles,
-                },
-            )
-        else:
-            self._update_relation_data(event.relation.id, {"database": self.database})
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the database relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the database is created
-        # (the database charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("database created at %s", datetime.now())
-            self.on.database_created.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "database_created")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “database_created“ is triggered.
-            return
-
-        # Emit an endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "endpoints_changed")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered.
-            return
-
-        # Emit a read only endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("read-only-endpoints changed on %s", datetime.now())
-            self.on.read_only_endpoints_changed.emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "read_only_endpoints_changed")
-
-
-# Kafka related events
-
-
-class KafkaProvidesEvent(RelationEvent):
-    """Base class for Kafka events."""
-
-    @property
-    def topic(self) -> Optional[str]:
-        """Returns the topic that was requested."""
-        return self.relation.data[self.relation.app].get("topic")
-
-
-class TopicRequestedEvent(KafkaProvidesEvent, ExtraRoleEvent):
-    """Event emitted when a new topic is requested for use on this relation."""
-
-
-class KafkaProvidesEvents(CharmEvents):
-    """Kafka events.
-
-    This class defines the events that the Kafka can emit.
-    """
-
-    topic_requested = EventSource(TopicRequestedEvent)
-
-
-class KafkaRequiresEvent(RelationEvent):
-    """Base class for Kafka events."""
-
-    @property
-    def bootstrap_server(self) -> Optional[str]:
-        """Returns a a comma-seperated list of broker uris."""
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def consumer_group_prefix(self) -> Optional[str]:
-        """Returns the consumer-group-prefix."""
-        return self.relation.data[self.relation.app].get("consumer-group-prefix")
-
-    @property
-    def zookeeper_uris(self) -> Optional[str]:
-        """Returns a comma separated list of Zookeeper uris."""
-        return self.relation.data[self.relation.app].get("zookeeper-uris")
-
-
-class TopicCreatedEvent(AuthenticationEvent, KafkaRequiresEvent):
-    """Event emitted when a new topic is created for use on this relation."""
-
-
-class BootstrapServerChangedEvent(AuthenticationEvent, KafkaRequiresEvent):
-    """Event emitted when the bootstrap server is changed."""
-
-
-class KafkaRequiresEvents(CharmEvents):
-    """Kafka events.
-
-    This class defines the events that the Kafka can emit.
-    """
-
-    topic_created = EventSource(TopicCreatedEvent)
-    bootstrap_server_changed = EventSource(BootstrapServerChangedEvent)
-
-
-# Kafka Provides and Requires
-
-
-class KafkaProvides(DataProvides):
-    """Provider-side of the Kafka relation."""
-
-    on = KafkaProvidesEvents()
-
-    def __init__(self, charm: CharmBase, relation_name: str) -> None:
-        super().__init__(charm, relation_name)
-
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the relation has changed."""
-        # Only the leader should handle this event.
-        if not self.local_unit.is_leader():
-            return
-
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Emit a topic requested event if the setup key (topic name and optional
-        # extra user roles) was added to the relation databag by the application.
-        if "topic" in diff.added:
-            self.on.topic_requested.emit(event.relation, app=event.app, unit=event.unit)
-
-    def set_bootstrap_server(self, relation_id: int, bootstrap_server: str) -> None:
-        """Set the bootstrap server in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            bootstrap_server: the bootstrap server address.
-        """
-        self._update_relation_data(relation_id, {"endpoints": bootstrap_server})
-
-    def set_consumer_group_prefix(self, relation_id: int, consumer_group_prefix: str) -> None:
-        """Set the consumer group prefix in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            consumer_group_prefix: the consumer group prefix string.
-        """
-        self._update_relation_data(relation_id, {"consumer-group-prefix": consumer_group_prefix})
-
-    def set_zookeeper_uris(self, relation_id: int, zookeeper_uris: str) -> None:
-        """Set the zookeeper uris in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            zookeeper_uris: comma-seperated list of ZooKeeper server uris.
-        """
-        self._update_relation_data(relation_id, {"zookeeper-uris": zookeeper_uris})
-
-
-class KafkaRequires(DataRequires):
-    """Requires-side of the Kafka relation."""
-
-    on = KafkaRequiresEvents()
-
-    def __init__(self, charm, relation_name: str, topic: str, extra_user_roles: str = None):
-        """Manager of Kafka client relations."""
-        # super().__init__(charm, relation_name)
-        super().__init__(charm, relation_name, extra_user_roles)
-        self.charm = charm
-        self.topic = topic
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the Kafka relation."""
-        # Sets both topic and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the topic.
-        self._update_relation_data(
-            event.relation.id,
-            {
-                "topic": self.topic,
-                "extra-user-roles": self.extra_user_roles,
-            }
-            if self.extra_user_roles is not None
-            else {"topic": self.topic},
-        )
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the Kafka relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the topic is created
-        # (the Kafka charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("topic created at %s", datetime.now())
-            self.on.topic_created.emit(event.relation, app=event.app, unit=event.unit)
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “topic_created“ is triggered.
-            return
-
-        # Emit an endpoints (bootstap-server) changed event if the Kakfa endpoints
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            self.on.bootstrap_server_changed.emit(
-                event.relation, app=event.app, unit=event.unit
-            )  # here check if this is the right design
-            return
diff --git a/installers/charm/osm-nbi/lib/charms/kafka_k8s/v0/kafka.py b/installers/charm/osm-nbi/lib/charms/kafka_k8s/v0/kafka.py
deleted file mode 100644 (file)
index aeb5edc..0000000
+++ /dev/null
@@ -1,200 +0,0 @@
-# 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.
-
-"""Kafka library.
-
-This [library](https://juju.is/docs/sdk/libraries) implements both sides of the
-`kafka` [interface](https://juju.is/docs/sdk/relations).
-
-The *provider* side of this interface is implemented by the
-[kafka-k8s Charmed Operator](https://charmhub.io/kafka-k8s).
-
-Any Charmed Operator that *requires* Kafka for providing its
-service should implement the *requirer* side of this interface.
-
-In a nutshell using this library to implement a Charmed Operator *requiring*
-Kafka would look like
-
-```
-$ charmcraft fetch-lib charms.kafka_k8s.v0.kafka
-```
-
-`metadata.yaml`:
-
-```
-requires:
-  kafka:
-    interface: kafka
-    limit: 1
-```
-
-`src/charm.py`:
-
-```
-from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
-from ops.charm import CharmBase
-
-
-class MyCharm(CharmBase):
-
-    on = KafkaEvents()
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.kafka = KafkaRequires(self)
-        self.framework.observe(
-            self.on.kafka_available,
-            self._on_kafka_available,
-        )
-        self.framework.observe(
-            self.on["kafka"].relation_broken,
-            self._on_kafka_broken,
-        )
-
-    def _on_kafka_available(self, event):
-        # Get Kafka host and port
-        host: str = self.kafka.host
-        port: int = self.kafka.port
-        # host => "kafka-k8s"
-        # port => 9092
-
-    def _on_kafka_broken(self, event):
-        # Stop service
-        # ...
-        self.unit.status = BlockedStatus("need kafka relation")
-```
-
-You can file bugs
-[here](https://github.com/charmed-osm/kafka-k8s-operator/issues)!
-"""
-
-from typing import Optional
-
-from ops.charm import CharmBase, CharmEvents
-from ops.framework import EventBase, EventSource, Object
-
-# The unique Charmhub library identifier, never change it
-from ops.model import Relation
-
-LIBID = "eacc8c85082347c9aae740e0220b8376"
-
-# 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 = 4
-
-
-KAFKA_HOST_APP_KEY = "host"
-KAFKA_PORT_APP_KEY = "port"
-
-
-class _KafkaAvailableEvent(EventBase):
-    """Event emitted when Kafka is available."""
-
-
-class KafkaEvents(CharmEvents):
-    """Kafka events.
-
-    This class defines the events that Kafka can emit.
-
-    Events:
-        kafka_available (_KafkaAvailableEvent)
-    """
-
-    kafka_available = EventSource(_KafkaAvailableEvent)
-
-
-class KafkaRequires(Object):
-    """Requires-side of the Kafka relation."""
-
-    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
-        super().__init__(charm, endpoint_name)
-        self.charm = charm
-        self._endpoint_name = endpoint_name
-
-        # Observe relation events
-        event_observe_mapping = {
-            charm.on[self._endpoint_name].relation_changed: self._on_relation_changed,
-        }
-        for event, observer in event_observe_mapping.items():
-            self.framework.observe(event, observer)
-
-    def _on_relation_changed(self, event) -> None:
-        if event.relation.app and all(
-            key in event.relation.data[event.relation.app]
-            for key in (KAFKA_HOST_APP_KEY, KAFKA_PORT_APP_KEY)
-        ):
-            self.charm.on.kafka_available.emit()
-
-    @property
-    def host(self) -> str:
-        """Get kafka hostname."""
-        relation: Relation = self.model.get_relation(self._endpoint_name)
-        return (
-            relation.data[relation.app].get(KAFKA_HOST_APP_KEY)
-            if relation and relation.app
-            else None
-        )
-
-    @property
-    def port(self) -> int:
-        """Get kafka port number."""
-        relation: Relation = self.model.get_relation(self._endpoint_name)
-        return (
-            int(relation.data[relation.app].get(KAFKA_PORT_APP_KEY))
-            if relation and relation.app
-            else None
-        )
-
-
-class KafkaProvides(Object):
-    """Provides-side of the Kafka relation."""
-
-    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
-        super().__init__(charm, endpoint_name)
-        self._endpoint_name = endpoint_name
-
-    def set_host_info(self, host: str, port: int, relation: Optional[Relation] = None) -> None:
-        """Set Kafka host and port.
-
-        This function writes in the application data of the relation, therefore,
-        only the unit leader can call it.
-
-        Args:
-            host (str): Kafka hostname or IP address.
-            port (int): Kafka port.
-            relation (Optional[Relation]): Relation to update.
-                                           If not specified, all relations will be updated.
-
-        Raises:
-            Exception: if a non-leader unit calls this function.
-        """
-        if not self.model.unit.is_leader():
-            raise Exception("only the leader set host information.")
-
-        if relation:
-            self._update_relation_data(host, port, relation)
-            return
-
-        for relation in self.model.relations[self._endpoint_name]:
-            self._update_relation_data(host, port, relation)
-
-    def _update_relation_data(self, host: str, port: int, relation: Relation) -> None:
-        """Update data in relation if needed."""
-        relation.data[self.model.app][KAFKA_HOST_APP_KEY] = host
-        relation.data[self.model.app][KAFKA_PORT_APP_KEY] = str(port)
diff --git a/installers/charm/osm-nbi/lib/charms/nginx_ingress_integrator/v0/ingress.py b/installers/charm/osm-nbi/lib/charms/nginx_ingress_integrator/v0/ingress.py
deleted file mode 100644 (file)
index be2d762..0000000
+++ /dev/null
@@ -1,229 +0,0 @@
-# See LICENSE file for licensing details.
-#   http://www.apache.org/licenses/LICENSE-2.0
-"""Library for the ingress relation.
-
-This library contains the Requires and Provides classes for handling
-the ingress interface.
-
-Import `IngressRequires` in your charm, with two required options:
-    - "self" (the charm itself)
-    - config_dict
-
-`config_dict` accepts the following keys:
-    - service-hostname (required)
-    - service-name (required)
-    - service-port (required)
-    - additional-hostnames
-    - limit-rps
-    - limit-whitelist
-    - max-body-size
-    - owasp-modsecurity-crs
-    - path-routes
-    - retry-errors
-    - rewrite-enabled
-    - rewrite-target
-    - service-namespace
-    - session-cookie-max-age
-    - tls-secret-name
-
-See [the config section](https://charmhub.io/nginx-ingress-integrator/configure) for descriptions
-of each, along with the required type.
-
-As an example, add the following to `src/charm.py`:
-```
-from charms.nginx_ingress_integrator.v0.ingress import IngressRequires
-
-# In your charm's `__init__` method.
-self.ingress = IngressRequires(self, {"service-hostname": self.config["external_hostname"],
-                                      "service-name": self.app.name,
-                                      "service-port": 80})
-
-# In your charm's `config-changed` handler.
-self.ingress.update_config({"service-hostname": self.config["external_hostname"]})
-```
-And then add the following to `metadata.yaml`:
-```
-requires:
-  ingress:
-    interface: ingress
-```
-You _must_ register the IngressRequires class as part of the `__init__` method
-rather than, for instance, a config-changed event handler. This is because
-doing so won't get the current relation changed event, because it wasn't
-registered to handle the event (because it wasn't created in `__init__` when
-the event was fired).
-"""
-
-import logging
-
-from ops.charm import CharmEvents
-from ops.framework import EventBase, EventSource, Object
-from ops.model import BlockedStatus
-
-# The unique Charmhub library identifier, never change it
-LIBID = "db0af4367506491c91663468fb5caa4c"
-
-# 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 = 10
-
-logger = logging.getLogger(__name__)
-
-REQUIRED_INGRESS_RELATION_FIELDS = {
-    "service-hostname",
-    "service-name",
-    "service-port",
-}
-
-OPTIONAL_INGRESS_RELATION_FIELDS = {
-    "additional-hostnames",
-    "limit-rps",
-    "limit-whitelist",
-    "max-body-size",
-    "owasp-modsecurity-crs",
-    "path-routes",
-    "retry-errors",
-    "rewrite-target",
-    "rewrite-enabled",
-    "service-namespace",
-    "session-cookie-max-age",
-    "tls-secret-name",
-}
-
-
-class IngressAvailableEvent(EventBase):
-    pass
-
-
-class IngressBrokenEvent(EventBase):
-    pass
-
-
-class IngressCharmEvents(CharmEvents):
-    """Custom charm events."""
-
-    ingress_available = EventSource(IngressAvailableEvent)
-    ingress_broken = EventSource(IngressBrokenEvent)
-
-
-class IngressRequires(Object):
-    """This class defines the functionality for the 'requires' side of the 'ingress' relation.
-
-    Hook events observed:
-        - relation-changed
-    """
-
-    def __init__(self, charm, config_dict):
-        super().__init__(charm, "ingress")
-
-        self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed)
-
-        self.config_dict = config_dict
-
-    def _config_dict_errors(self, update_only=False):
-        """Check our config dict for errors."""
-        blocked_message = "Error in ingress relation, check `juju debug-log`"
-        unknown = [
-            x
-            for x in self.config_dict
-            if x not in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
-        ]
-        if unknown:
-            logger.error(
-                "Ingress relation error, unknown key(s) in config dictionary found: %s",
-                ", ".join(unknown),
-            )
-            self.model.unit.status = BlockedStatus(blocked_message)
-            return True
-        if not update_only:
-            missing = [x for x in REQUIRED_INGRESS_RELATION_FIELDS if x not in self.config_dict]
-            if missing:
-                logger.error(
-                    "Ingress relation error, missing required key(s) in config dictionary: %s",
-                    ", ".join(sorted(missing)),
-                )
-                self.model.unit.status = BlockedStatus(blocked_message)
-                return True
-        return False
-
-    def _on_relation_changed(self, event):
-        """Handle the relation-changed event."""
-        # `self.unit` isn't available here, so use `self.model.unit`.
-        if self.model.unit.is_leader():
-            if self._config_dict_errors():
-                return
-            for key in self.config_dict:
-                event.relation.data[self.model.app][key] = str(self.config_dict[key])
-
-    def update_config(self, config_dict):
-        """Allow for updates to relation."""
-        if self.model.unit.is_leader():
-            self.config_dict = config_dict
-            if self._config_dict_errors(update_only=True):
-                return
-            relation = self.model.get_relation("ingress")
-            if relation:
-                for key in self.config_dict:
-                    relation.data[self.model.app][key] = str(self.config_dict[key])
-
-
-class IngressProvides(Object):
-    """This class defines the functionality for the 'provides' side of the 'ingress' relation.
-
-    Hook events observed:
-        - relation-changed
-    """
-
-    def __init__(self, charm):
-        super().__init__(charm, "ingress")
-        # Observe the relation-changed hook event and bind
-        # self.on_relation_changed() to handle the event.
-        self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed)
-        self.framework.observe(charm.on["ingress"].relation_broken, self._on_relation_broken)
-        self.charm = charm
-
-    def _on_relation_changed(self, event):
-        """Handle a change to the ingress relation.
-
-        Confirm we have the fields we expect to receive."""
-        # `self.unit` isn't available here, so use `self.model.unit`.
-        if not self.model.unit.is_leader():
-            return
-
-        ingress_data = {
-            field: event.relation.data[event.app].get(field)
-            for field in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
-        }
-
-        missing_fields = sorted(
-            [
-                field
-                for field in REQUIRED_INGRESS_RELATION_FIELDS
-                if ingress_data.get(field) is None
-            ]
-        )
-
-        if missing_fields:
-            logger.error(
-                "Missing required data fields for ingress relation: {}".format(
-                    ", ".join(missing_fields)
-                )
-            )
-            self.model.unit.status = BlockedStatus(
-                "Missing fields for ingress: {}".format(", ".join(missing_fields))
-            )
-
-        # Create an event that our charm can use to decide it's okay to
-        # configure the ingress.
-        self.charm.on.ingress_available.emit()
-
-    def _on_relation_broken(self, _):
-        """Handle a relation-broken event in the ingress relation."""
-        if not self.model.unit.is_leader():
-            return
-
-        # Create an event that our charm can use to remove the ingress resource.
-        self.charm.on.ingress_broken.emit()
diff --git a/installers/charm/osm-nbi/lib/charms/observability_libs/v1/kubernetes_service_patch.py b/installers/charm/osm-nbi/lib/charms/observability_libs/v1/kubernetes_service_patch.py
deleted file mode 100644 (file)
index 506dbf0..0000000
+++ /dev/null
@@ -1,291 +0,0 @@
-# Copyright 2021 Canonical Ltd.
-# See LICENSE file for licensing details.
-#   http://www.apache.org/licenses/LICENSE-2.0
-
-"""# KubernetesServicePatch Library.
-
-This library is designed to enable developers to more simply patch the Kubernetes Service created
-by Juju during the deployment of a sidecar charm. When sidecar charms are deployed, Juju creates a
-service named after the application in the namespace (named after the Juju model). This service by
-default contains a "placeholder" port, which is 65536/TCP.
-
-When modifying the default set of resources managed by Juju, one must consider the lifecycle of the
-charm. In this case, any modifications to the default service (created during deployment), will be
-overwritten during a charm upgrade.
-
-When initialised, this library binds a handler to the parent charm's `install` and `upgrade_charm`
-events which applies the patch to the cluster. This should ensure that the service ports are
-correct throughout the charm's life.
-
-The constructor simply takes a reference to the parent charm, and a list of
-[`lightkube`](https://github.com/gtsystem/lightkube) ServicePorts that each define a port for the
-service. For information regarding the `lightkube` `ServicePort` model, please visit the
-`lightkube` [docs](https://gtsystem.github.io/lightkube-models/1.23/models/core_v1/#serviceport).
-
-Optionally, a name of the service (in case service name needs to be patched as well), labels,
-selectors, and annotations can be provided as keyword arguments.
-
-## Getting Started
-
-To get started using the library, you just need to fetch the library using `charmcraft`. **Note
-that you also need to add `lightkube` and `lightkube-models` to your charm's `requirements.txt`.**
-
-```shell
-cd some-charm
-charmcraft fetch-lib charms.observability_libs.v0.kubernetes_service_patch
-echo <<-EOF >> requirements.txt
-lightkube
-lightkube-models
-EOF
-```
-
-Then, to initialise the library:
-
-For `ClusterIP` services:
-
-```python
-# ...
-from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
-from lightkube.models.core_v1 import ServicePort
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    port = ServicePort(443, name=f"{self.app.name}")
-    self.service_patcher = KubernetesServicePatch(self, [port])
-    # ...
-```
-
-For `LoadBalancer`/`NodePort` services:
-
-```python
-# ...
-from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
-from lightkube.models.core_v1 import ServicePort
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    port = ServicePort(443, name=f"{self.app.name}", targetPort=443, nodePort=30666)
-    self.service_patcher = KubernetesServicePatch(
-        self, [port], "LoadBalancer"
-    )
-    # ...
-```
-
-Port protocols can also be specified. Valid protocols are `"TCP"`, `"UDP"`, and `"SCTP"`
-
-```python
-# ...
-from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
-from lightkube.models.core_v1 import ServicePort
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    tcp = ServicePort(443, name=f"{self.app.name}-tcp", protocol="TCP")
-    udp = ServicePort(443, name=f"{self.app.name}-udp", protocol="UDP")
-    sctp = ServicePort(443, name=f"{self.app.name}-sctp", protocol="SCTP")
-    self.service_patcher = KubernetesServicePatch(self, [tcp, udp, sctp])
-    # ...
-```
-
-Additionally, you may wish to use mocks in your charm's unit testing to ensure that the library
-does not try to make any API calls, or open any files during testing that are unlikely to be
-present, and could break your tests. The easiest way to do this is during your test `setUp`:
-
-```python
-# ...
-
-@patch("charm.KubernetesServicePatch", lambda x, y: None)
-def setUp(self, *unused):
-    self.harness = Harness(SomeCharm)
-    # ...
-```
-"""
-
-import logging
-from types import MethodType
-from typing import List, Literal
-
-from lightkube import ApiError, Client
-from lightkube.models.core_v1 import ServicePort, ServiceSpec
-from lightkube.models.meta_v1 import ObjectMeta
-from lightkube.resources.core_v1 import Service
-from lightkube.types import PatchType
-from ops.charm import CharmBase
-from ops.framework import Object
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0042f86d0a874435adef581806cddbbb"
-
-# 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 = 1
-
-ServiceType = Literal["ClusterIP", "LoadBalancer"]
-
-
-class KubernetesServicePatch(Object):
-    """A utility for patching the Kubernetes service set up by Juju."""
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        ports: List[ServicePort],
-        service_name: str = None,
-        service_type: ServiceType = "ClusterIP",
-        additional_labels: dict = None,
-        additional_selectors: dict = None,
-        additional_annotations: dict = None,
-    ):
-        """Constructor for KubernetesServicePatch.
-
-        Args:
-            charm: the charm that is instantiating the library.
-            ports: a list of ServicePorts
-            service_name: allows setting custom name to the patched service. If none given,
-                application name will be used.
-            service_type: desired type of K8s service. Default value is in line with ServiceSpec's
-                default value.
-            additional_labels: Labels to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_selectors: Selectors to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_annotations: Annotations to be added to the kubernetes service.
-        """
-        super().__init__(charm, "kubernetes-service-patch")
-        self.charm = charm
-        self.service_name = service_name if service_name else self._app
-        self.service = self._service_object(
-            ports,
-            service_name,
-            service_type,
-            additional_labels,
-            additional_selectors,
-            additional_annotations,
-        )
-
-        # Make mypy type checking happy that self._patch is a method
-        assert isinstance(self._patch, MethodType)
-        # Ensure this patch is applied during the 'install' and 'upgrade-charm' events
-        self.framework.observe(charm.on.install, self._patch)
-        self.framework.observe(charm.on.upgrade_charm, self._patch)
-
-    def _service_object(
-        self,
-        ports: List[ServicePort],
-        service_name: str = None,
-        service_type: ServiceType = "ClusterIP",
-        additional_labels: dict = None,
-        additional_selectors: dict = None,
-        additional_annotations: dict = None,
-    ) -> Service:
-        """Creates a valid Service representation.
-
-        Args:
-            ports: a list of ServicePorts
-            service_name: allows setting custom name to the patched service. If none given,
-                application name will be used.
-            service_type: desired type of K8s service. Default value is in line with ServiceSpec's
-                default value.
-            additional_labels: Labels to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_selectors: Selectors to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_annotations: Annotations to be added to the kubernetes service.
-
-        Returns:
-            Service: A valid representation of a Kubernetes Service with the correct ports.
-        """
-        if not service_name:
-            service_name = self._app
-        labels = {"app.kubernetes.io/name": self._app}
-        if additional_labels:
-            labels.update(additional_labels)
-        selector = {"app.kubernetes.io/name": self._app}
-        if additional_selectors:
-            selector.update(additional_selectors)
-        return Service(
-            apiVersion="v1",
-            kind="Service",
-            metadata=ObjectMeta(
-                namespace=self._namespace,
-                name=service_name,
-                labels=labels,
-                annotations=additional_annotations,  # type: ignore[arg-type]
-            ),
-            spec=ServiceSpec(
-                selector=selector,
-                ports=ports,
-                type=service_type,
-            ),
-        )
-
-    def _patch(self, _) -> None:
-        """Patch the Kubernetes service created by Juju to map the correct port.
-
-        Raises:
-            PatchFailed: if patching fails due to lack of permissions, or otherwise.
-        """
-        if not self.charm.unit.is_leader():
-            return
-
-        client = Client()
-        try:
-            if self.service_name != self._app:
-                self._delete_and_create_service(client)
-            client.patch(Service, self.service_name, self.service, patch_type=PatchType.MERGE)
-        except ApiError as e:
-            if e.status.code == 403:
-                logger.error("Kubernetes service patch failed: `juju trust` this application.")
-            else:
-                logger.error("Kubernetes service patch failed: %s", str(e))
-        else:
-            logger.info("Kubernetes service '%s' patched successfully", self._app)
-
-    def _delete_and_create_service(self, client: Client):
-        service = client.get(Service, self._app, namespace=self._namespace)
-        service.metadata.name = self.service_name  # type: ignore[attr-defined]
-        service.metadata.resourceVersion = service.metadata.uid = None  # type: ignore[attr-defined]   # noqa: E501
-        client.delete(Service, self._app, namespace=self._namespace)
-        client.create(service)
-
-    def is_patched(self) -> bool:
-        """Reports if the service patch has been applied.
-
-        Returns:
-            bool: A boolean indicating if the service patch has been applied.
-        """
-        client = Client()
-        # Get the relevant service from the cluster
-        service = client.get(Service, name=self.service_name, namespace=self._namespace)
-        # Construct a list of expected ports, should the patch be applied
-        expected_ports = [(p.port, p.targetPort) for p in self.service.spec.ports]
-        # Construct a list in the same manner, using the fetched service
-        fetched_ports = [(p.port, p.targetPort) for p in service.spec.ports]  # type: ignore[attr-defined]  # noqa: E501
-        return expected_ports == fetched_ports
-
-    @property
-    def _app(self) -> str:
-        """Name of the current Juju application.
-
-        Returns:
-            str: A string containing the name of the current Juju application.
-        """
-        return self.charm.app.name
-
-    @property
-    def _namespace(self) -> str:
-        """The Kubernetes namespace we're running in.
-
-        Returns:
-            str: A string containing the name of the current Kubernetes namespace.
-        """
-        with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") as f:
-            return f.read().strip()
diff --git a/installers/charm/osm-nbi/lib/charms/osm_libs/v0/utils.py b/installers/charm/osm-nbi/lib/charms/osm_libs/v0/utils.py
deleted file mode 100644 (file)
index d739ba6..0000000
+++ /dev/null
@@ -1,544 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-#         http://www.apache.org/licenses/LICENSE-2.0
-"""OSM Utils Library.
-
-This library offers some utilities made for but not limited to Charmed OSM.
-
-# Getting started
-
-Execute the following command inside your Charmed Operator folder to fetch the library.
-
-```shell
-charmcraft fetch-lib charms.osm_libs.v0.utils
-```
-
-# CharmError Exception
-
-An exception that takes to arguments, the message and the StatusBase class, which are useful
-to set the status of the charm when the exception raises.
-
-Example:
-```shell
-from charms.osm_libs.v0.utils import CharmError
-
-class MyCharm(CharmBase):
-    def _on_config_changed(self, _):
-        try:
-            if not self.config.get("some-option"):
-                raise CharmError("need some-option", BlockedStatus)
-
-            if not self.mysql_ready:
-                raise CharmError("waiting for mysql", WaitingStatus)
-
-            # Do stuff...
-
-        exception CharmError as e:
-            self.unit.status = e.status
-```
-
-# Pebble validations
-
-The `check_container_ready` function checks that a container is ready,
-and therefore Pebble is ready.
-
-The `check_service_active` function checks that a service in a container is running.
-
-Both functions raise a CharmError if the validations fail.
-
-Example:
-```shell
-from charms.osm_libs.v0.utils import check_container_ready, check_service_active
-
-class MyCharm(CharmBase):
-    def _on_config_changed(self, _):
-        try:
-            container: Container = self.unit.get_container("my-container")
-            check_container_ready(container)
-            check_service_active(container, "my-service")
-            # Do stuff...
-
-        exception CharmError as e:
-            self.unit.status = e.status
-```
-
-# Debug-mode
-
-The debug-mode allows OSM developers to easily debug OSM modules.
-
-Example:
-```shell
-from charms.osm_libs.v0.utils import DebugMode
-
-class MyCharm(CharmBase):
-    _stored = StoredState()
-
-    def __init__(self, _):
-        # ...
-        container: Container = self.unit.get_container("my-container")
-        hostpaths = [
-            HostPath(
-                config="module-hostpath",
-                container_path="/usr/lib/python3/dist-packages/module"
-            ),
-        ]
-        vscode_workspace_path = "files/vscode-workspace.json"
-        self.debug_mode = DebugMode(
-            self,
-            self._stored,
-            container,
-            hostpaths,
-            vscode_workspace_path,
-        )
-
-    def _on_update_status(self, _):
-        if self.debug_mode.started:
-            return
-        # ...
-
-    def _get_debug_mode_information(self):
-        command = self.debug_mode.command
-        password = self.debug_mode.password
-        return command, password
-```
-
-# More
-
-- Get pod IP with `get_pod_ip()`
-"""
-from dataclasses import dataclass
-import logging
-import secrets
-import socket
-from pathlib import Path
-from typing import List
-
-from lightkube import Client
-from lightkube.models.core_v1 import HostPathVolumeSource, Volume, VolumeMount
-from lightkube.resources.apps_v1 import StatefulSet
-from ops.charm import CharmBase
-from ops.framework import Object, StoredState
-from ops.model import (
-    ActiveStatus,
-    BlockedStatus,
-    Container,
-    MaintenanceStatus,
-    StatusBase,
-    WaitingStatus,
-)
-from ops.pebble import ServiceStatus
-
-# The unique Charmhub library identifier, never change it
-LIBID = "e915908eebee4cdd972d484728adf984"
-
-# 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 = 5
-
-logger = logging.getLogger(__name__)
-
-
-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)
-
-
-def check_container_ready(container: Container) -> None:
-    """Check Pebble has started in the container.
-
-    Args:
-        container (Container): Container to be checked.
-
-    Raises:
-        CharmError: if container is not ready.
-    """
-    if not container.can_connect():
-        raise CharmError("waiting for pebble to start", MaintenanceStatus)
-
-
-def check_service_active(container: Container, service_name: str) -> None:
-    """Check if the service is running.
-
-    Args:
-        container (Container): Container to be checked.
-        service_name (str): Name of the service to check.
-
-    Raises:
-        CharmError: if the service is not running.
-    """
-    if service_name not in container.get_plan().services:
-        raise CharmError(f"{service_name} service not configured yet", WaitingStatus)
-
-    if container.get_service(service_name).current != ServiceStatus.ACTIVE:
-        raise CharmError(f"{service_name} service is not running")
-
-
-def get_pod_ip() -> str:
-    """Get Kubernetes Pod IP.
-
-    Returns:
-        str: The IP of the Pod.
-    """
-    return socket.gethostbyname(socket.gethostname())
-
-
-_DEBUG_SCRIPT = r"""#!/bin/bash
-# Install SSH
-
-function download_code(){{
-    wget https://go.microsoft.com/fwlink/?LinkID=760868 -O code.deb
-}}
-
-function setup_envs(){{
-    grep "source /debug.envs" /root/.bashrc || echo "source /debug.envs" | tee -a /root/.bashrc
-}}
-function setup_ssh(){{
-    apt install ssh -y
-    cat /etc/ssh/sshd_config |
-        grep -E '^PermitRootLogin yes$$' || (
-        echo PermitRootLogin yes |
-        tee -a /etc/ssh/sshd_config
-    )
-    service ssh stop
-    sleep 3
-    service ssh start
-    usermod --password $(echo {} | openssl passwd -1 -stdin) root
-}}
-
-function setup_code(){{
-    apt install libasound2 -y
-    (dpkg -i code.deb || apt-get install -f -y || apt-get install -f -y) && echo Code installed successfully
-    code --install-extension ms-python.python --user-data-dir /root
-    mkdir -p /root/.vscode-server
-    cp -R /root/.vscode/extensions /root/.vscode-server/extensions
-}}
-
-export DEBIAN_FRONTEND=noninteractive
-apt update && apt install wget -y
-download_code &
-setup_ssh &
-setup_envs
-wait
-setup_code &
-wait
-"""
-
-
-@dataclass
-class SubModule:
-    """Represent RO Submodules."""
-    sub_module_path: str
-    container_path: str
-
-
-class HostPath:
-    """Represents a hostpath."""
-    def __init__(self, config: str, container_path: str, submodules: dict = None) -> None:
-        mount_path_items = config.split("-")
-        mount_path_items.reverse()
-        self.mount_path = "/" + "/".join(mount_path_items)
-        self.config = config
-        self.sub_module_dict = {}
-        if submodules:
-            for submodule in submodules.keys():
-                self.sub_module_dict[submodule] = SubModule(
-                    sub_module_path=self.mount_path + "/" + submodule + "/" + submodules[submodule].split("/")[-1],
-                    container_path=submodules[submodule],
-                )
-        else:
-            self.container_path = container_path
-            self.module_name = container_path.split("/")[-1]
-
-class DebugMode(Object):
-    """Class to handle the debug-mode."""
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        stored: StoredState,
-        container: Container,
-        hostpaths: List[HostPath] = [],
-        vscode_workspace_path: str = "files/vscode-workspace.json",
-    ) -> None:
-        super().__init__(charm, "debug-mode")
-
-        self.charm = charm
-        self._stored = stored
-        self.hostpaths = hostpaths
-        self.vscode_workspace = Path(vscode_workspace_path).read_text()
-        self.container = container
-
-        self._stored.set_default(
-            debug_mode_started=False,
-            debug_mode_vscode_command=None,
-            debug_mode_password=None,
-        )
-
-        self.framework.observe(self.charm.on.config_changed, self._on_config_changed)
-        self.framework.observe(self.charm.on[container.name].pebble_ready, self._on_config_changed)
-        self.framework.observe(self.charm.on.update_status, self._on_update_status)
-
-    def _on_config_changed(self, _) -> None:
-        """Handler for the config-changed event."""
-        if not self.charm.unit.is_leader():
-            return
-
-        debug_mode_enabled = self.charm.config.get("debug-mode", False)
-        action = self.enable if debug_mode_enabled else self.disable
-        action()
-
-    def _on_update_status(self, _) -> None:
-        """Handler for the update-status event."""
-        if not self.charm.unit.is_leader() or not self.started:
-            return
-
-        self.charm.unit.status = ActiveStatus("debug-mode: ready")
-
-    @property
-    def started(self) -> bool:
-        """Indicates whether the debug-mode has started or not."""
-        return self._stored.debug_mode_started
-
-    @property
-    def command(self) -> str:
-        """Command to launch vscode."""
-        return self._stored.debug_mode_vscode_command
-
-    @property
-    def password(self) -> str:
-        """SSH password."""
-        return self._stored.debug_mode_password
-
-    def enable(self, service_name: str = None) -> None:
-        """Enable debug-mode.
-
-        This function mounts hostpaths of the OSM modules (if set), and
-        configures the container so it can be easily debugged. The setup
-        includes the configuration of SSH, environment variables, and
-        VSCode workspace and plugins.
-
-        Args:
-            service_name (str, optional): Pebble service name which has the desired environment
-                variables. Mandatory if there is more than one Pebble service configured.
-        """
-        hostpaths_to_reconfigure = self._hostpaths_to_reconfigure()
-        if self.started and not hostpaths_to_reconfigure:
-            self.charm.unit.status = ActiveStatus("debug-mode: ready")
-            return
-
-        logger.debug("enabling debug-mode")
-
-        # Mount hostpaths if set.
-        # If hostpaths are mounted, the statefulset will be restarted,
-        # and for that reason we return immediately. On restart, the hostpaths
-        # won't be mounted and then we can continue and setup the debug-mode.
-        if hostpaths_to_reconfigure:
-            self.charm.unit.status = MaintenanceStatus("debug-mode: configuring hostpaths")
-            self._configure_hostpaths(hostpaths_to_reconfigure)
-            return
-
-        self.charm.unit.status = MaintenanceStatus("debug-mode: starting")
-        password = secrets.token_hex(8)
-        self._setup_debug_mode(
-            password,
-            service_name,
-            mounted_hostpaths=[hp for hp in self.hostpaths if self.charm.config.get(hp.config)],
-        )
-
-        self._stored.debug_mode_vscode_command = self._get_vscode_command(get_pod_ip())
-        self._stored.debug_mode_password = password
-        self._stored.debug_mode_started = True
-        logger.info("debug-mode is ready")
-        self.charm.unit.status = ActiveStatus("debug-mode: ready")
-
-    def disable(self) -> None:
-        """Disable debug-mode."""
-        logger.debug("disabling debug-mode")
-        current_status = self.charm.unit.status
-        hostpaths_unmounted = self._unmount_hostpaths()
-
-        if not self._stored.debug_mode_started:
-            return
-        self._stored.debug_mode_started = False
-        self._stored.debug_mode_vscode_command = None
-        self._stored.debug_mode_password = None
-
-        if not hostpaths_unmounted:
-            self.charm.unit.status = current_status
-            self._restart()
-
-    def _hostpaths_to_reconfigure(self) -> List[HostPath]:
-        hostpaths_to_reconfigure: List[HostPath] = []
-        client = Client()
-        statefulset = client.get(StatefulSet, self.charm.app.name, namespace=self.charm.model.name)
-        volumes = statefulset.spec.template.spec.volumes
-
-        for hostpath in self.hostpaths:
-            hostpath_is_set = True if self.charm.config.get(hostpath.config) else False
-            hostpath_already_configured = next(
-                (True for volume in volumes if volume.name == hostpath.config), False
-            )
-            if hostpath_is_set != hostpath_already_configured:
-                hostpaths_to_reconfigure.append(hostpath)
-
-        return hostpaths_to_reconfigure
-
-    def _setup_debug_mode(
-        self,
-        password: str,
-        service_name: str = None,
-        mounted_hostpaths: List[HostPath] = [],
-    ) -> None:
-        services = self.container.get_plan().services
-        if not service_name and len(services) != 1:
-            raise Exception("Cannot start debug-mode: please set the service_name")
-
-        service = None
-        if not service_name:
-            service_name, service = services.popitem()
-        if not service:
-            service = services.get(service_name)
-
-        logger.debug(f"getting environment variables from service {service_name}")
-        environment = service.environment
-        environment_file_content = "\n".join(
-            [f'export {key}="{value}"' for key, value in environment.items()]
-        )
-        logger.debug(f"pushing environment file to {self.container.name} container")
-        self.container.push("/debug.envs", environment_file_content)
-
-        # Push VSCode workspace
-        logger.debug(f"pushing vscode workspace to {self.container.name} container")
-        self.container.push("/debug.code-workspace", self.vscode_workspace)
-
-        # Execute debugging script
-        logger.debug(f"pushing debug-mode setup script to {self.container.name} container")
-        self.container.push("/debug.sh", _DEBUG_SCRIPT.format(password), permissions=0o777)
-        logger.debug(f"executing debug-mode setup script in {self.container.name} container")
-        self.container.exec(["/debug.sh"]).wait_output()
-        logger.debug(f"stopping service {service_name} in {self.container.name} container")
-        self.container.stop(service_name)
-
-        # Add symlinks to mounted hostpaths
-        for hostpath in mounted_hostpaths:
-            logger.debug(f"adding symlink for {hostpath.config}")
-            if len(hostpath.sub_module_dict) > 0:
-                for sub_module in hostpath.sub_module_dict.keys():
-                    self.container.exec(["rm", "-rf", hostpath.sub_module_dict[sub_module].container_path]).wait_output()
-                    self.container.exec(
-                        [
-                            "ln",
-                            "-s",
-                            hostpath.sub_module_dict[sub_module].sub_module_path,
-                            hostpath.sub_module_dict[sub_module].container_path,
-                        ]
-                    )
-
-            else:
-                self.container.exec(["rm", "-rf", hostpath.container_path]).wait_output()
-                self.container.exec(
-                    [
-                        "ln",
-                        "-s",
-                        f"{hostpath.mount_path}/{hostpath.module_name}",
-                        hostpath.container_path,
-                    ]
-                )
-
-    def _configure_hostpaths(self, hostpaths: List[HostPath]):
-        client = Client()
-        statefulset = client.get(StatefulSet, self.charm.app.name, namespace=self.charm.model.name)
-
-        for hostpath in hostpaths:
-            if self.charm.config.get(hostpath.config):
-                self._add_hostpath_to_statefulset(hostpath, statefulset)
-            else:
-                self._delete_hostpath_from_statefulset(hostpath, statefulset)
-
-        client.replace(statefulset)
-
-    def _unmount_hostpaths(self) -> bool:
-        client = Client()
-        hostpath_unmounted = False
-        statefulset = client.get(StatefulSet, self.charm.app.name, namespace=self.charm.model.name)
-
-        for hostpath in self.hostpaths:
-            if self._delete_hostpath_from_statefulset(hostpath, statefulset):
-                hostpath_unmounted = True
-
-        if hostpath_unmounted:
-            client.replace(statefulset)
-
-        return hostpath_unmounted
-
-    def _add_hostpath_to_statefulset(self, hostpath: HostPath, statefulset: StatefulSet):
-        # Add volume
-        logger.debug(f"adding volume {hostpath.config} to {self.charm.app.name} statefulset")
-        volume = Volume(
-            hostpath.config,
-            hostPath=HostPathVolumeSource(
-                path=self.charm.config[hostpath.config],
-                type="Directory",
-            ),
-        )
-        statefulset.spec.template.spec.volumes.append(volume)
-
-        # Add volumeMount
-        for statefulset_container in statefulset.spec.template.spec.containers:
-            if statefulset_container.name != self.container.name:
-                continue
-
-            logger.debug(
-                f"adding volumeMount {hostpath.config} to {self.container.name} container"
-            )
-            statefulset_container.volumeMounts.append(
-                VolumeMount(mountPath=hostpath.mount_path, name=hostpath.config)
-            )
-
-    def _delete_hostpath_from_statefulset(self, hostpath: HostPath, statefulset: StatefulSet):
-        hostpath_unmounted = False
-        for volume in statefulset.spec.template.spec.volumes:
-
-            if hostpath.config != volume.name:
-                continue
-
-            # Remove volumeMount
-            for statefulset_container in statefulset.spec.template.spec.containers:
-                if statefulset_container.name != self.container.name:
-                    continue
-                for volume_mount in statefulset_container.volumeMounts:
-                    if volume_mount.name != hostpath.config:
-                        continue
-
-                    logger.debug(
-                        f"removing volumeMount {hostpath.config} from {self.container.name} container"
-                    )
-                    statefulset_container.volumeMounts.remove(volume_mount)
-
-            # Remove volume
-            logger.debug(
-                f"removing volume {hostpath.config} from {self.charm.app.name} statefulset"
-            )
-            statefulset.spec.template.spec.volumes.remove(volume)
-
-            hostpath_unmounted = True
-        return hostpath_unmounted
-
-    def _get_vscode_command(
-        self,
-        pod_ip: str,
-        user: str = "root",
-        workspace_path: str = "/debug.code-workspace",
-    ) -> str:
-        return f"code --remote ssh-remote+{user}@{pod_ip} {workspace_path}"
-
-    def _restart(self):
-        self.container.exec(["kill", "-HUP", "1"])
diff --git a/installers/charm/osm-nbi/lib/charms/osm_nbi/v0/nbi.py b/installers/charm/osm-nbi/lib/charms/osm_nbi/v0/nbi.py
deleted file mode 100644 (file)
index 130b6fa..0000000
+++ /dev/null
@@ -1,178 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# Learn more at: https://juju.is/docs/sdk
-
-"""Nbi library.
-
-This [library](https://juju.is/docs/sdk/libraries) implements both sides of the
-`nbi` [interface](https://juju.is/docs/sdk/relations).
-
-The *provider* side of this interface is implemented by the
-[osm-nbi Charmed Operator](https://charmhub.io/osm-nbi).
-
-Any Charmed Operator that *requires* NBI for providing its
-service should implement the *requirer* side of this interface.
-
-In a nutshell using this library to implement a Charmed Operator *requiring*
-NBI would look like
-
-```
-$ charmcraft fetch-lib charms.osm_nbi.v0.nbi
-```
-
-`metadata.yaml`:
-
-```
-requires:
-  nbi:
-    interface: nbi
-    limit: 1
-```
-
-`src/charm.py`:
-
-```
-from charms.osm_nbi.v0.nbi import NbiRequires
-from ops.charm import CharmBase
-
-
-class MyCharm(CharmBase):
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.nbi = NbiRequires(self)
-        self.framework.observe(
-            self.on["nbi"].relation_changed,
-            self._on_nbi_relation_changed,
-        )
-        self.framework.observe(
-            self.on["nbi"].relation_broken,
-            self._on_nbi_relation_broken,
-        )
-        self.framework.observe(
-            self.on["nbi"].relation_broken,
-            self._on_nbi_broken,
-        )
-
-    def _on_nbi_relation_broken(self, event):
-        # Get NBI host and port
-        host: str = self.nbi.host
-        port: int = self.nbi.port
-        # host => "osm-nbi"
-        # port => 9999
-
-    def _on_nbi_broken(self, event):
-        # Stop service
-        # ...
-        self.unit.status = BlockedStatus("need nbi relation")
-```
-
-You can file bugs
-[here](https://osm.etsi.org/bugzilla/enter_bug.cgi), selecting the `devops` module!
-"""
-from typing import Optional
-
-from ops.charm import CharmBase, CharmEvents
-from ops.framework import EventBase, EventSource, Object
-from ops.model import Relation
-
-
-# The unique Charmhub library identifier, never change it
-LIBID = "8c888f7c869949409e12c16d78ec068b"
-
-# 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 = 1
-
-NBI_HOST_APP_KEY = "host"
-NBI_PORT_APP_KEY = "port"
-
-
-class NbiRequires(Object):  # pragma: no cover
-    """Requires-side of the Nbi relation."""
-
-    def __init__(self, charm: CharmBase, endpoint_name: str = "nbi") -> None:
-        super().__init__(charm, endpoint_name)
-        self.charm = charm
-        self._endpoint_name = endpoint_name
-
-    @property
-    def host(self) -> str:
-        """Get nbi hostname."""
-        relation: Relation = self.model.get_relation(self._endpoint_name)
-        return (
-            relation.data[relation.app].get(NBI_HOST_APP_KEY)
-            if relation and relation.app
-            else None
-        )
-
-    @property
-    def port(self) -> int:
-        """Get nbi port number."""
-        relation: Relation = self.model.get_relation(self._endpoint_name)
-        return (
-            int(relation.data[relation.app].get(NBI_PORT_APP_KEY))
-            if relation and relation.app
-            else None
-        )
-
-
-class NbiProvides(Object):
-    """Provides-side of the Nbi relation."""
-
-    def __init__(self, charm: CharmBase, endpoint_name: str = "nbi") -> None:
-        super().__init__(charm, endpoint_name)
-        self._endpoint_name = endpoint_name
-
-    def set_host_info(self, host: str, port: int, relation: Optional[Relation] = None) -> None:
-        """Set Nbi host and port.
-
-        This function writes in the application data of the relation, therefore,
-        only the unit leader can call it.
-
-        Args:
-            host (str): Nbi hostname or IP address.
-            port (int): Nbi port.
-            relation (Optional[Relation]): Relation to update.
-                                           If not specified, all relations will be updated.
-
-        Raises:
-            Exception: if a non-leader unit calls this function.
-        """
-        if not self.model.unit.is_leader():
-            raise Exception("only the leader set host information.")
-
-        if relation:
-            self._update_relation_data(host, port, relation)
-            return
-
-        for relation in self.model.relations[self._endpoint_name]:
-            self._update_relation_data(host, port, relation)
-
-    def _update_relation_data(self, host: str, port: int, relation: Relation) -> None:
-        """Update data in relation if needed."""
-        relation.data[self.model.app][NBI_HOST_APP_KEY] = host
-        relation.data[self.model.app][NBI_PORT_APP_KEY] = str(port)
diff --git a/installers/charm/osm-nbi/metadata.yaml b/installers/charm/osm-nbi/metadata.yaml
deleted file mode 100644 (file)
index 8a336c8..0000000
+++ /dev/null
@@ -1,79 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Overview on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
-
-name: osm-nbi
-
-# The following metadata are human-readable and will be published prominently on Charmhub.
-
-display-name: OSM NBI
-
-summary: OSM Northbound Interface (NBI)
-
-description: |
-  A Kubernetes operator that deploys the Northbound Interface of OSM.
-
-  OSM provides a unified northbound interface (NBI), based on NFV SOL005, which enables
-  the full operation of system and the Network Services and Network Slices under its control.
-
-  In fact, OSM’s NBI offers the service of managing the lifecycle of Network Services (NS)
-  and Network Slices Instances (NSI), providing as a service all the necessary abstractions
-  to allow the complete control, operation and supervision of the NS/NSI lifecycle by client
-  systems, avoiding the exposure of unnecessary details of its constituent elements.
-
-  This charm doesn't make sense on its own.
-  See more:
-    - https://charmhub.io/osm
-
-containers:
-  nbi:
-    resource: nbi-image
-
-# This file populates the Resources tab on Charmhub.
-
-resources:
-  nbi-image:
-    type: oci-image
-    description: OCI image for nbi
-    upstream-source: opensourcemano/nbi
-
-requires:
-  kafka:
-    interface: kafka
-    limit: 1
-  mongodb:
-    interface: mongodb_client
-    limit: 1
-  keystone:
-    interface: keystone
-    limit: 1
-  prometheus:
-    interface: prometheus
-    limit: 1
-  ingress:
-    interface: ingress
-    limit: 1
-
-provides:
-  nbi:
-    interface: nbi
diff --git a/installers/charm/osm-nbi/pyproject.toml b/installers/charm/osm-nbi/pyproject.toml
deleted file mode 100644 (file)
index 16cf0f4..0000000
+++ /dev/null
@@ -1,52 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-
-# 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", "E501", "D107"]
-# D100, D101, D102, D103: Ignore missing docstrings in tests
-per-file-ignores = ["tests/*:D100,D101,D102,D103,D104"]
-docstring-convention = "google"
diff --git a/installers/charm/osm-nbi/requirements.txt b/installers/charm/osm-nbi/requirements.txt
deleted file mode 100644 (file)
index 761edd8..0000000
+++ /dev/null
@@ -1,23 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-ops < 2.2
-lightkube
-lightkube-models
-git+https://github.com/charmed-osm/config-validator/
diff --git a/installers/charm/osm-nbi/src/charm.py b/installers/charm/osm-nbi/src/charm.py
deleted file mode 100755 (executable)
index b19beae..0000000
+++ /dev/null
@@ -1,314 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# Learn more at: https://juju.is/docs/sdk
-
-"""OSM NBI charm.
-
-See more: https://charmhub.io/osm
-"""
-
-import logging
-from typing import Any, Dict
-
-from charms.data_platform_libs.v0.data_interfaces import DatabaseRequires
-from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
-from charms.nginx_ingress_integrator.v0.ingress import IngressRequires
-from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
-from charms.osm_libs.v0.utils import (
-    CharmError,
-    DebugMode,
-    HostPath,
-    check_container_ready,
-    check_service_active,
-)
-from charms.osm_nbi.v0.nbi import NbiProvides
-from lightkube.models.core_v1 import ServicePort
-from ops.charm import ActionEvent, CharmBase, RelationJoinedEvent
-from ops.framework import StoredState
-from ops.main import main
-from ops.model import ActiveStatus, Container
-
-from legacy_interfaces import KeystoneClient, PrometheusClient
-
-HOSTPATHS = [
-    HostPath(
-        config="nbi-hostpath",
-        container_path="/usr/lib/python3/dist-packages/osm_nbi",
-    ),
-    HostPath(
-        config="common-hostpath",
-        container_path="/usr/lib/python3/dist-packages/osm_common",
-    ),
-]
-SERVICE_PORT = 9999
-
-logger = logging.getLogger(__name__)
-
-
-class OsmNbiCharm(CharmBase):
-    """OSM NBI Kubernetes sidecar charm."""
-
-    on = KafkaEvents()
-    _stored = StoredState()
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.ingress = IngressRequires(
-            self,
-            {
-                "service-hostname": self.external_hostname,
-                "service-name": self.app.name,
-                "service-port": SERVICE_PORT,
-            },
-        )
-        self.kafka = KafkaRequires(self)
-        self.nbi = NbiProvides(self)
-        self.mongodb_client = DatabaseRequires(
-            self, "mongodb", database_name="osm", extra_user_roles="admin"
-        )
-        self.prometheus_client = PrometheusClient(self, "prometheus")
-        self.keystone_client = KeystoneClient(self, "keystone")
-        self._observe_charm_events()
-        self.container: Container = self.unit.get_container("nbi")
-        self.debug_mode = DebugMode(self, self._stored, self.container, HOSTPATHS)
-        self._patch_k8s_service()
-
-    @property
-    def external_hostname(self) -> str:
-        """External hostname property.
-
-        Returns:
-            str: the external hostname from config.
-                If not set, return the ClusterIP service name.
-        """
-        return self.config.get("external-hostname") or self.app.name
-
-    # ---------------------------------------------------------------------------
-    #   Handlers for Charm Events
-    # ---------------------------------------------------------------------------
-
-    def _on_config_changed(self, _) -> None:
-        """Handler for the config-changed event."""
-        try:
-            self._validate_config()
-            self._check_relations()
-            # Check if the container is ready.
-            # Eventually it will become ready after the first pebble-ready event.
-            check_container_ready(self.container)
-
-            if not self.debug_mode.started:
-                self._configure_service(self.container)
-            self._update_ingress_config()
-            self._update_nbi_relation()
-            # Update charm status
-            self._on_update_status()
-        except CharmError as e:
-            logger.debug(e.message)
-            self.unit.status = e.status
-
-    def _on_update_status(self, _=None) -> None:
-        """Handler for the update-status event."""
-        try:
-            self._check_relations()
-            if self.debug_mode.started:
-                return
-            check_container_ready(self.container)
-            check_service_active(self.container, "nbi")
-            self.unit.status = ActiveStatus()
-        except CharmError as e:
-            logger.debug(e.message)
-            self.unit.status = e.status
-
-    def _on_required_relation_broken(self, _) -> None:
-        """Handler for the kafka-broken event."""
-        # Check Pebble has started in the container
-        try:
-            check_container_ready(self.container)
-            check_service_active(self.container, "nbi")
-            self.container.stop("nbi")
-        except CharmError:
-            pass
-        finally:
-            self._on_update_status()
-
-    def _update_nbi_relation(self, event: RelationJoinedEvent = None) -> None:
-        """Handler for the nbi-relation-joined event."""
-        if self.unit.is_leader():
-            self.nbi.set_host_info(self.app.name, SERVICE_PORT, event.relation if event else None)
-
-    def _on_get_debug_mode_information_action(self, event: ActionEvent) -> None:
-        """Handler for the get-debug-mode-information action event."""
-        if not self.debug_mode.started:
-            event.fail("debug-mode has not started. Hint: juju config nbi debug-mode=true")
-            return
-
-        debug_info = {"command": self.debug_mode.command, "password": self.debug_mode.password}
-        event.set_results(debug_info)
-
-    # ---------------------------------------------------------------------------
-    #   Validation and configuration and more
-    # ---------------------------------------------------------------------------
-
-    def _patch_k8s_service(self) -> None:
-        port = ServicePort(SERVICE_PORT, name=f"{self.app.name}")
-        self.service_patcher = KubernetesServicePatch(self, [port])
-
-    def _observe_charm_events(self) -> None:
-        event_handler_mapping = {
-            # Core lifecycle events
-            self.on.nbi_pebble_ready: self._on_config_changed,
-            self.on.config_changed: self._on_config_changed,
-            self.on.update_status: self._on_update_status,
-            # Relation events
-            self.on.kafka_available: self._on_config_changed,
-            self.on["kafka"].relation_broken: self._on_required_relation_broken,
-            self.mongodb_client.on.database_created: self._on_config_changed,
-            self.on["mongodb"].relation_broken: self._on_required_relation_broken,
-            # Action events
-            self.on.get_debug_mode_information_action: self._on_get_debug_mode_information_action,
-            self.on.nbi_relation_joined: self._update_nbi_relation,
-        }
-        for relation in [self.on[rel_name] for rel_name in ["prometheus", "keystone"]]:
-            event_handler_mapping[relation.relation_changed] = self._on_config_changed
-            event_handler_mapping[relation.relation_broken] = self._on_required_relation_broken
-
-        for event, handler in event_handler_mapping.items():
-            self.framework.observe(event, handler)
-
-    def _is_database_available(self) -> bool:
-        try:
-            return self.mongodb_client.is_resource_created()
-        except KeyError:
-            return False
-
-    def _validate_config(self) -> None:
-        """Validate charm configuration.
-
-        Raises:
-            CharmError: if charm configuration is invalid.
-        """
-        logger.debug("validating charm config")
-
-    def _check_relations(self) -> None:
-        """Validate charm relations.
-
-        Raises:
-            CharmError: if charm configuration is invalid.
-        """
-        logger.debug("check for missing relations")
-        missing_relations = []
-
-        if not self.kafka.host or not self.kafka.port:
-            missing_relations.append("kafka")
-        if not self._is_database_available():
-            missing_relations.append("mongodb")
-        if self.prometheus_client.is_missing_data_in_app():
-            missing_relations.append("prometheus")
-        if self.keystone_client.is_missing_data_in_app():
-            missing_relations.append("keystone")
-
-        if missing_relations:
-            relations_str = ", ".join(missing_relations)
-            one_relation_missing = len(missing_relations) == 1
-            error_msg = f'need {relations_str} relation{"" if one_relation_missing else "s"}'
-            logger.warning(error_msg)
-            raise CharmError(error_msg)
-
-    def _update_ingress_config(self) -> None:
-        """Update ingress config in relation."""
-        ingress_config = {
-            "service-hostname": self.external_hostname,
-            "max-body-size": self.config["max-body-size"],
-        }
-        if "tls-secret-name" in self.config:
-            ingress_config["tls-secret-name"] = self.config["tls-secret-name"]
-        logger.debug(f"updating ingress-config: {ingress_config}")
-        self.ingress.update_config(ingress_config)
-
-    def _configure_service(self, container: Container) -> None:
-        """Add Pebble layer with the nbi service."""
-        logger.debug(f"configuring {self.app.name} service")
-        container.add_layer("nbi", self._get_layer(), combine=True)
-        container.replan()
-
-    def _get_layer(self) -> Dict[str, Any]:
-        """Get layer for Pebble."""
-        return {
-            "summary": "nbi layer",
-            "description": "pebble config layer for nbi",
-            "services": {
-                "nbi": {
-                    "override": "replace",
-                    "summary": "nbi service",
-                    "command": "/bin/sh -c 'cd /app/osm_nbi && python3 -m osm_nbi.nbi'",  # cd /app/osm_nbi is needed until we upgrade Juju to 3.x
-                    "startup": "enabled",
-                    "user": "appuser",
-                    "group": "appuser",
-                    "working-dir": "/app/osm_nbi",  # This parameter has no effect in juju 2.9.x
-                    "environment": {
-                        # General configuration
-                        "OSMNBI_SERVER_ENABLE_TEST": False,
-                        "OSMNBI_STATIC_DIR": "/app/osm_nbi/html_public",
-                        # Kafka configuration
-                        "OSMNBI_MESSAGE_HOST": self.kafka.host,
-                        "OSMNBI_MESSAGE_PORT": self.kafka.port,
-                        "OSMNBI_MESSAGE_DRIVER": "kafka",
-                        # Database configuration
-                        "OSMNBI_DATABASE_DRIVER": "mongo",
-                        "OSMNBI_DATABASE_URI": self._get_mongodb_uri(),
-                        "OSMNBI_DATABASE_COMMONKEY": self.config["database-commonkey"],
-                        # Storage configuration
-                        "OSMNBI_STORAGE_DRIVER": "mongo",
-                        "OSMNBI_STORAGE_PATH": "/app/storage",
-                        "OSMNBI_STORAGE_COLLECTION": "files",
-                        "OSMNBI_STORAGE_URI": self._get_mongodb_uri(),
-                        # Prometheus configuration
-                        "OSMNBI_PROMETHEUS_HOST": self.prometheus_client.hostname,
-                        "OSMNBI_PROMETHEUS_PORT": self.prometheus_client.port,
-                        # Log configuration
-                        "OSMNBI_LOG_LEVEL": self.config["log-level"],
-                        # Authentication environments
-                        "OSMNBI_AUTHENTICATION_BACKEND": "keystone",
-                        "OSMNBI_AUTHENTICATION_AUTH_URL": self.keystone_client.host,
-                        "OSMNBI_AUTHENTICATION_AUTH_PORT": self.keystone_client.port,
-                        "OSMNBI_AUTHENTICATION_USER_DOMAIN_NAME": self.keystone_client.user_domain_name,
-                        "OSMNBI_AUTHENTICATION_PROJECT_DOMAIN_NAME": self.keystone_client.project_domain_name,
-                        "OSMNBI_AUTHENTICATION_SERVICE_USERNAME": self.keystone_client.username,
-                        "OSMNBI_AUTHENTICATION_SERVICE_PASSWORD": self.keystone_client.password,
-                        "OSMNBI_AUTHENTICATION_SERVICE_PROJECT": self.keystone_client.service,
-                        # DISABLING INTERNAL SSL SERVER
-                        "OSMNBI_SERVER_SSL_MODULE": "",
-                        "OSMNBI_SERVER_SSL_CERTIFICATE": "",
-                        "OSMNBI_SERVER_SSL_PRIVATE_KEY": "",
-                        "OSMNBI_SERVER_SSL_PASS_PHRASE": "",
-                    },
-                }
-            },
-        }
-
-    def _get_mongodb_uri(self):
-        return list(self.mongodb_client.fetch_relation_data().values())[0]["uris"]
-
-
-if __name__ == "__main__":  # pragma: no cover
-    main(OsmNbiCharm)
diff --git a/installers/charm/osm-nbi/src/legacy_interfaces.py b/installers/charm/osm-nbi/src/legacy_interfaces.py
deleted file mode 100644 (file)
index 5deb3f5..0000000
+++ /dev/null
@@ -1,205 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-# flake8: noqa
-
-import ops
-
-
-class BaseRelationClient(ops.framework.Object):
-    """Requires side of a Kafka Endpoint"""
-
-    def __init__(
-        self,
-        charm: ops.charm.CharmBase,
-        relation_name: str,
-        mandatory_fields: list = [],
-    ):
-        super().__init__(charm, relation_name)
-        self.relation_name = relation_name
-        self.mandatory_fields = mandatory_fields
-        self._update_relation()
-
-    def get_data_from_unit(self, key: str):
-        if not self.relation:
-            # This update relation doesn't seem to be needed, but I added it because apparently
-            # the data is empty in the unit tests.
-            # In reality, the constructor is called in every hook.
-            # In the unit tests when doing an update_relation_data, apparently it is not called.
-            self._update_relation()
-        if self.relation:
-            for unit in self.relation.units:
-                data = self.relation.data[unit].get(key)
-                if data:
-                    return data
-
-    def get_data_from_app(self, key: str):
-        if not self.relation or self.relation.app not in self.relation.data:
-            # This update relation doesn't seem to be needed, but I added it because apparently
-            # the data is empty in the unit tests.
-            # In reality, the constructor is called in every hook.
-            # In the unit tests when doing an update_relation_data, apparently it is not called.
-            self._update_relation()
-        if self.relation and self.relation.app in self.relation.data:
-            data = self.relation.data[self.relation.app].get(key)
-            if data:
-                return data
-
-    def is_missing_data_in_unit(self):
-        return not all([self.get_data_from_unit(field) for field in self.mandatory_fields])
-
-    def is_missing_data_in_app(self):
-        return not all([self.get_data_from_app(field) for field in self.mandatory_fields])
-
-    def _update_relation(self):
-        self.relation = self.framework.model.get_relation(self.relation_name)
-
-
-class KeystoneClient(BaseRelationClient):
-    """Requires side of a Keystone Endpoint"""
-
-    mandatory_fields = [
-        "host",
-        "port",
-        "user_domain_name",
-        "project_domain_name",
-        "username",
-        "password",
-        "service",
-        "keystone_db_password",
-        "region_id",
-        "admin_username",
-        "admin_password",
-        "admin_project_name",
-    ]
-
-    def __init__(self, charm: ops.charm.CharmBase, relation_name: str):
-        super().__init__(charm, relation_name, self.mandatory_fields)
-
-    @property
-    def host(self):
-        return self.get_data_from_app("host")
-
-    @property
-    def port(self):
-        return self.get_data_from_app("port")
-
-    @property
-    def user_domain_name(self):
-        return self.get_data_from_app("user_domain_name")
-
-    @property
-    def project_domain_name(self):
-        return self.get_data_from_app("project_domain_name")
-
-    @property
-    def username(self):
-        return self.get_data_from_app("username")
-
-    @property
-    def password(self):
-        return self.get_data_from_app("password")
-
-    @property
-    def service(self):
-        return self.get_data_from_app("service")
-
-    @property
-    def keystone_db_password(self):
-        return self.get_data_from_app("keystone_db_password")
-
-    @property
-    def region_id(self):
-        return self.get_data_from_app("region_id")
-
-    @property
-    def admin_username(self):
-        return self.get_data_from_app("admin_username")
-
-    @property
-    def admin_password(self):
-        return self.get_data_from_app("admin_password")
-
-    @property
-    def admin_project_name(self):
-        return self.get_data_from_app("admin_project_name")
-
-
-class MongoClient(BaseRelationClient):
-    """Requires side of a Mongo Endpoint"""
-
-    mandatory_fields_mapping = {
-        "reactive": ["connection_string"],
-        "ops": ["replica_set_uri", "replica_set_name"],
-    }
-
-    def __init__(self, charm: ops.charm.CharmBase, relation_name: str):
-        super().__init__(charm, relation_name, mandatory_fields=[])
-
-    @property
-    def connection_string(self):
-        if self.is_opts():
-            replica_set_uri = self.get_data_from_unit("replica_set_uri")
-            replica_set_name = self.get_data_from_unit("replica_set_name")
-            return f"{replica_set_uri}?replicaSet={replica_set_name}"
-        else:
-            return self.get_data_from_unit("connection_string")
-
-    def is_opts(self):
-        return not self.is_missing_data_in_unit_ops()
-
-    def is_missing_data_in_unit(self):
-        return self.is_missing_data_in_unit_ops() and self.is_missing_data_in_unit_reactive()
-
-    def is_missing_data_in_unit_ops(self):
-        return not all(
-            [self.get_data_from_unit(field) for field in self.mandatory_fields_mapping["ops"]]
-        )
-
-    def is_missing_data_in_unit_reactive(self):
-        return not all(
-            [self.get_data_from_unit(field) for field in self.mandatory_fields_mapping["reactive"]]
-        )
-
-
-class PrometheusClient(BaseRelationClient):
-    """Requires side of a Prometheus Endpoint"""
-
-    mandatory_fields = ["hostname", "port"]
-
-    def __init__(self, charm: ops.charm.CharmBase, relation_name: str):
-        super().__init__(charm, relation_name, self.mandatory_fields)
-
-    @property
-    def hostname(self):
-        return self.get_data_from_app("hostname")
-
-    @property
-    def port(self):
-        return self.get_data_from_app("port")
-
-    @property
-    def user(self):
-        return self.get_data_from_app("user")
-
-    @property
-    def password(self):
-        return self.get_data_from_app("password")
diff --git a/installers/charm/osm-nbi/tests/integration/test_charm.py b/installers/charm/osm-nbi/tests/integration/test_charm.py
deleted file mode 100644 (file)
index 8555175..0000000
+++ /dev/null
@@ -1,203 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-# Learn more about testing at: https://juju.is/docs/sdk/testing
-
-import asyncio
-import logging
-import shlex
-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())
-NBI_APP = METADATA["name"]
-KAFKA_CHARM = "kafka-k8s"
-KAFKA_APP = "kafka"
-MARIADB_CHARM = "charmed-osm-mariadb-k8s"
-MARIADB_APP = "mariadb"
-MONGO_DB_CHARM = "mongodb-k8s"
-MONGO_DB_APP = "mongodb"
-KEYSTONE_CHARM = "osm-keystone"
-KEYSTONE_APP = "keystone"
-PROMETHEUS_CHARM = "osm-prometheus"
-PROMETHEUS_APP = "prometheus"
-ZOOKEEPER_CHARM = "zookeeper-k8s"
-ZOOKEEPER_APP = "zookeeper"
-INGRESS_CHARM = "nginx-ingress-integrator"
-INGRESS_APP = "ingress"
-APPS = [KAFKA_APP, MONGO_DB_APP, MARIADB_APP, ZOOKEEPER_APP, KEYSTONE_APP, PROMETHEUS_APP, NBI_APP]
-
-
-@pytest.mark.abort_on_fail
-async def test_nbi_is_deployed(ops_test: OpsTest):
-    charm = await ops_test.build_charm(".")
-    resources = {"nbi-image": METADATA["resources"]["nbi-image"]["upstream-source"]}
-
-    await asyncio.gather(
-        ops_test.model.deploy(
-            charm, resources=resources, application_name=NBI_APP, series="jammy"
-        ),
-        ops_test.model.deploy(KAFKA_CHARM, application_name=KAFKA_APP, channel="stable"),
-        ops_test.model.deploy(MONGO_DB_CHARM, application_name=MONGO_DB_APP, channel="5/edge"),
-        ops_test.model.deploy(MARIADB_CHARM, application_name=MARIADB_APP, channel="stable"),
-        ops_test.model.deploy(ZOOKEEPER_CHARM, application_name=ZOOKEEPER_APP, channel="stable"),
-        ops_test.model.deploy(PROMETHEUS_CHARM, application_name=PROMETHEUS_APP, channel="stable"),
-    )
-    # Keystone charm has to be deployed differently since
-    # bug https://github.com/juju/python-libjuju/issues/766
-    # prevents setting correctly the resources
-    keystone_image = "opensourcemano/keystone:testing-daily"
-    cmd = f"juju deploy {KEYSTONE_CHARM} {KEYSTONE_APP} --resource keystone-image={keystone_image} --channel=latest/beta --series jammy"
-    await ops_test.run(*shlex.split(cmd), check=True)
-
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS,
-        )
-    assert ops_test.model.applications[NBI_APP].status == "blocked"
-    unit = ops_test.model.applications[NBI_APP].units[0]
-    assert unit.workload_status_message == "need kafka, mongodb, prometheus, keystone relations"
-
-    logger.info("Adding relations for other components")
-    await ops_test.model.add_relation(KAFKA_APP, ZOOKEEPER_APP)
-    await ops_test.model.add_relation(MARIADB_APP, KEYSTONE_APP)
-
-    logger.info("Adding relations for NBI")
-    await ops_test.model.add_relation(
-        "{}:mongodb".format(NBI_APP), "{}:database".format(MONGO_DB_APP)
-    )
-    await ops_test.model.add_relation(NBI_APP, KAFKA_APP)
-    await ops_test.model.add_relation(NBI_APP, PROMETHEUS_APP)
-    await ops_test.model.add_relation(NBI_APP, KEYSTONE_APP)
-
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS,
-            status="active",
-        )
-
-
-@pytest.mark.abort_on_fail
-async def test_nbi_scales_up(ops_test: OpsTest):
-    logger.info("Scaling up osm-nbi")
-    expected_units = 3
-    assert len(ops_test.model.applications[NBI_APP].units) == 1
-    await ops_test.model.applications[NBI_APP].scale(expected_units)
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=[NBI_APP], status="active", wait_for_exact_units=expected_units
-        )
-
-
-@pytest.mark.abort_on_fail
-@pytest.mark.parametrize(
-    "relation_to_remove", [KAFKA_APP, MONGO_DB_APP, PROMETHEUS_APP, KEYSTONE_APP]
-)
-async def test_nbi_blocks_without_relation(ops_test: OpsTest, relation_to_remove):
-    logger.info("Removing relation: %s", relation_to_remove)
-    # mongoDB relation is named "database"
-    local_relation = relation_to_remove
-    if local_relation == MONGO_DB_APP:
-        local_relation = "database"
-    await asyncio.gather(
-        ops_test.model.applications[relation_to_remove].remove_relation(local_relation, NBI_APP)
-    )
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(apps=[NBI_APP])
-    assert ops_test.model.applications[NBI_APP].status == "blocked"
-    for unit in ops_test.model.applications[NBI_APP].units:
-        assert unit.workload_status_message == f"need {relation_to_remove} relation"
-    await ops_test.model.add_relation(NBI_APP, relation_to_remove)
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS,
-            status="active",
-        )
-
-
-@pytest.mark.abort_on_fail
-async def test_nbi_action_debug_mode_disabled(ops_test: OpsTest):
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS,
-            status="active",
-        )
-    logger.info("Running action 'get-debug-mode-information'")
-    action = (
-        await ops_test.model.applications[NBI_APP]
-        .units[0]
-        .run_action("get-debug-mode-information")
-    )
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(apps=[NBI_APP])
-    status = await ops_test.model.get_action_status(uuid_or_prefix=action.entity_id)
-    assert status[action.entity_id] == "failed"
-
-
-@pytest.mark.abort_on_fail
-async def test_nbi_action_debug_mode_enabled(ops_test: OpsTest):
-    await ops_test.model.applications[NBI_APP].set_config({"debug-mode": "true"})
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS,
-            status="active",
-        )
-    logger.info("Running action 'get-debug-mode-information'")
-    # list of units is not ordered
-    unit_id = list(
-        filter(
-            lambda x: (x.entity_id == f"{NBI_APP}/0"), ops_test.model.applications[NBI_APP].units
-        )
-    )[0]
-    action = await unit_id.run_action("get-debug-mode-information")
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(apps=[NBI_APP])
-    status = await ops_test.model.get_action_status(uuid_or_prefix=action.entity_id)
-    message = await ops_test.model.get_action_output(action_uuid=action.entity_id)
-    assert status[action.entity_id] == "completed"
-    assert "command" in message
-    assert "password" in message
-
-
-@pytest.mark.abort_on_fail
-async def test_nbi_integration_ingress(ops_test: OpsTest):
-    # Temporal workaround due to python-libjuju 2.9.42.2 bug fixed in
-    # https://github.com/juju/python-libjuju/pull/854
-    # To be replaced when juju version 2.9.43 is used.
-    cmd = f"juju deploy {INGRESS_CHARM} {INGRESS_APP} --channel stable"
-    await ops_test.run(*shlex.split(cmd), check=True)
-
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS + [INGRESS_APP],
-        )
-
-    await ops_test.model.add_relation(NBI_APP, INGRESS_APP)
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS + [INGRESS_APP],
-            status="active",
-        )
diff --git a/installers/charm/osm-nbi/tests/unit/test_charm.py b/installers/charm/osm-nbi/tests/unit/test_charm.py
deleted file mode 100644 (file)
index b160419..0000000
+++ /dev/null
@@ -1,124 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-# Learn more about testing at: https://juju.is/docs/sdk/testing
-
-import pytest
-from ops.model import ActiveStatus, BlockedStatus
-from ops.testing import Harness
-from pytest_mock import MockerFixture
-
-from charm import CharmError, OsmNbiCharm, check_service_active
-
-container_name = "nbi"
-service_name = "nbi"
-
-
-@pytest.fixture
-def harness(mocker: MockerFixture):
-    mocker.patch("charm.KubernetesServicePatch", lambda x, y: None)
-    harness = Harness(OsmNbiCharm)
-    harness.begin()
-    harness.container_pebble_ready(container_name)
-    yield harness
-    harness.cleanup()
-
-
-def test_missing_relations(harness: Harness):
-    harness.charm.on.config_changed.emit()
-    assert type(harness.charm.unit.status) == BlockedStatus
-    assert all(
-        relation in harness.charm.unit.status.message
-        for relation in ["mongodb", "kafka", "prometheus", "keystone"]
-    )
-
-
-def test_ready(harness: Harness):
-    _add_relations(harness)
-    assert harness.charm.unit.status == ActiveStatus()
-
-
-def test_container_stops_after_relation_broken(harness: Harness):
-    harness.charm.on[container_name].pebble_ready.emit(container_name)
-    container = harness.charm.unit.get_container(container_name)
-    relation_ids = _add_relations(harness)
-    check_service_active(container, service_name)
-    harness.remove_relation(relation_ids[0])
-    with pytest.raises(CharmError):
-        check_service_active(container, service_name)
-
-
-def test_nbi_relation_joined(harness: Harness):
-    harness.set_leader(True)
-    _add_relations(harness)
-    relation_id = harness.add_relation("nbi", "ng-ui")
-    harness.add_relation_unit(relation_id, "ng-ui/0")
-    relation_data = harness.get_relation_data(relation_id, harness.charm.app.name)
-    assert harness.charm.unit.status == ActiveStatus()
-    assert relation_data == {"host": harness.charm.app.name, "port": "9999"}
-
-
-def _add_relations(harness: Harness):
-    relation_ids = []
-    # Add mongo relation
-    relation_id = harness.add_relation("mongodb", "mongodb")
-    harness.add_relation_unit(relation_id, "mongodb/0")
-    harness.update_relation_data(
-        relation_id,
-        "mongodb",
-        {"uris": "mongodb://:1234", "username": "user", "password": "password"},
-    )
-    relation_ids.append(relation_id)
-    # Add kafka relation
-    relation_id = harness.add_relation("kafka", "kafka")
-    harness.add_relation_unit(relation_id, "kafka/0")
-    harness.update_relation_data(relation_id, "kafka", {"host": "kafka", "port": "9092"})
-    relation_ids.append(relation_id)
-    # Add prometheus relation
-    relation_id = harness.add_relation("prometheus", "prometheus")
-    harness.add_relation_unit(relation_id, "prometheus/0")
-    harness.update_relation_data(
-        relation_id, "prometheus", {"hostname": "prometheus", "port": "9090"}
-    )
-    relation_ids.append(relation_id)
-    # Add keystone relation
-    relation_id = harness.add_relation("keystone", "keystone")
-    harness.add_relation_unit(relation_id, "keystone/0")
-    harness.update_relation_data(
-        relation_id,
-        "keystone",
-        {
-            "host": "host",
-            "port": "port",
-            "user_domain_name": "user_domain_name",
-            "project_domain_name": "project_domain_name",
-            "username": "username",
-            "password": "password",
-            "service": "service",
-            "keystone_db_password": "keystone_db_password",
-            "region_id": "region_id",
-            "admin_username": "admin_username",
-            "admin_password": "admin_password",
-            "admin_project_name": "admin_project_name",
-        },
-    )
-    relation_ids.append(relation_id)
-    return relation_ids
diff --git a/installers/charm/osm-nbi/tox.ini b/installers/charm/osm-nbi/tox.ini
deleted file mode 100644 (file)
index 07ea16d..0000000
+++ /dev/null
@@ -1,95 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-
-[tox]
-skipsdist=True
-skip_missing_interpreters = True
-envlist = lint, unit, integration
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-lib_path = {toxinidir}/lib/charms/osm_nbi
-all_path = {[vars]src_path} {[vars]tst_path} 
-
-[testenv]
-basepython = python3.8
-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-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    # uncomment the following line if this charm owns a lib
-    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
-    # 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:integration]
-description = Run integration tests
-deps =
-    pytest
-    juju<3
-    pytest-operator
-    -r{toxinidir}/requirements.txt
-commands =
-    pytest -v --tb native --ignore={[vars]tst_path}unit --log-cli-level=INFO -s {posargs} --cloud microk8s
diff --git a/installers/charm/osm-ng-ui/.gitignore b/installers/charm/osm-ng-ui/.gitignore
deleted file mode 100644 (file)
index 87d0a58..0000000
+++ /dev/null
@@ -1,29 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-venv/
-build/
-*.charm
-.tox/
-.coverage
-coverage.xml
-__pycache__/
-*.py[cod]
-.vscode
\ No newline at end of file
diff --git a/installers/charm/osm-ng-ui/.jujuignore b/installers/charm/osm-ng-ui/.jujuignore
deleted file mode 100644 (file)
index 17c7a8b..0000000
+++ /dev/null
@@ -1,23 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-/venv
-*.py[cod]
-*.charm
diff --git a/installers/charm/osm-ng-ui/CONTRIBUTING.md b/installers/charm/osm-ng-ui/CONTRIBUTING.md
deleted file mode 100644 (file)
index 8a91a44..0000000
+++ /dev/null
@@ -1,78 +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.
-
-For those usages not covered by the Apache License, Version 2.0 please
-contact: legal@canonical.com
-
-To get in touch with the maintainers, please contact:
-osm-charmers@lists.launchpad.net -->
-
-# Contributing
-
-## Overview
-
-This documents explains the processes and practices recommended for contributing enhancements to
-this operator.
-
-- Generally, before developing enhancements to this charm, you should consider [opening an issue
-  ](https://osm.etsi.org/bugzilla/enter_bug.cgi?product=OSM) explaining your use case. (Component=devops, version=master)
-- If you would like to chat with us about your use-cases or proposed implementation, you can reach
-  us at [OSM Juju public channel](https://opensourcemano.slack.com/archives/C027KJGPECA).
-- 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 gerrit patch onto
-  the `master` branch.
-
-## 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 dev
-# Enable DEBUG logging
-juju model-config logging-config="<root>=INFO;unit=DEBUG"
-# Deploy the charm
-juju deploy ./osm-ng-ui_ubuntu-22.04-amd64.charm \
-    --resource ng-ui-image=opensourcemano/ng-ui:testing-daily --series jammy
-```
diff --git a/installers/charm/osm-ng-ui/LICENSE b/installers/charm/osm-ng-ui/LICENSE
deleted file mode 100644 (file)
index 7e9d504..0000000
+++ /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 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.
diff --git a/installers/charm/osm-ng-ui/README.md b/installers/charm/osm-ng-ui/README.md
deleted file mode 100644 (file)
index 20a6f76..0000000
+++ /dev/null
@@ -1,43 +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.
-
-For those usages not covered by the Apache License, Version 2.0 please
-contact: legal@canonical.com
-
-To get in touch with the maintainers, please contact:
-osm-charmers@lists.launchpad.net -->
-
-<!-- 
-Avoid using this README file for information that is maintained or published elsewhere, e.g.:
-
-* metadata.yaml > published on Charmhub
-* documentation > published on (or linked to from) Charmhub
-* detailed contribution guide > documentation or CONTRIBUTING.md
-
-Use links instead. 
--->
-
-# OSM NBI
-
-Charmhub package name: osm-ng-ui
-More information: https://charmhub.io/osm-ng-ui
-
-## Other resources
-
-* [Read more](https://osm.etsi.org/docs/user-guide/latest/) 
-
-* [Contributing](https://osm.etsi.org/gitweb/?p=osm/devops.git;a=blob;f=installers/charm/osm-ng-ui/CONTRIBUTING.md)
-
-* See the [Juju SDK documentation](https://juju.is/docs/sdk) for more information about developing and improving charms.
-                                                           
diff --git a/installers/charm/osm-ng-ui/actions.yaml b/installers/charm/osm-ng-ui/actions.yaml
deleted file mode 100644 (file)
index 6d52c05..0000000
+++ /dev/null
@@ -1,23 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Actions tab on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
diff --git a/installers/charm/osm-ng-ui/charmcraft.yaml b/installers/charm/osm-ng-ui/charmcraft.yaml
deleted file mode 100644 (file)
index 072529c..0000000
+++ /dev/null
@@ -1,34 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-
-type: charm
-bases:
-  - build-on:
-      - name: "ubuntu"
-        channel: "22.04"
-    run-on:
-      - name: "ubuntu"
-        channel: "22.04"
-
-parts:
-  charm:
-    build-packages:
-      - git
diff --git a/installers/charm/osm-ng-ui/config.yaml b/installers/charm/osm-ng-ui/config.yaml
deleted file mode 100644 (file)
index 31ffd84..0000000
+++ /dev/null
@@ -1,50 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Configure tab on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
-
-options:
-  # Ingress options
-  external-hostname:
-    default: ""
-    description: |
-      The url that will be configured in the Kubernetes ingress.
-
-      The easiest way of configuring the external-hostname without having the DNS setup is by using
-      a Wildcard DNS like nip.io constructing the url like so:
-        - ng-ui.127.0.0.1.nip.io (valid within the K8s cluster node)
-        - ng-ui.<k8s-worker-ip>.nip.io (valid from outside the K8s cluster node)
-
-      This option is only applicable when the Kubernetes cluster has nginx ingress configured
-      and the charm is related to the nginx-ingress-integrator.
-      See more: https://charmhub.io/nginx-ingress-integrator
-    type: string
-  max-body-size:
-    default: 20
-    description: Max allowed body-size (for file uploads) in megabytes, set to 0 to
-      disable limits.
-    source: default
-    type: int
-    value: 20
-  tls-secret-name:
-    description: TLS secret name to use for ingress.
-    type: string
diff --git a/installers/charm/osm-ng-ui/lib/charms/nginx_ingress_integrator/v0/ingress.py b/installers/charm/osm-ng-ui/lib/charms/nginx_ingress_integrator/v0/ingress.py
deleted file mode 100644 (file)
index be2d762..0000000
+++ /dev/null
@@ -1,229 +0,0 @@
-# See LICENSE file for licensing details.
-#   http://www.apache.org/licenses/LICENSE-2.0
-"""Library for the ingress relation.
-
-This library contains the Requires and Provides classes for handling
-the ingress interface.
-
-Import `IngressRequires` in your charm, with two required options:
-    - "self" (the charm itself)
-    - config_dict
-
-`config_dict` accepts the following keys:
-    - service-hostname (required)
-    - service-name (required)
-    - service-port (required)
-    - additional-hostnames
-    - limit-rps
-    - limit-whitelist
-    - max-body-size
-    - owasp-modsecurity-crs
-    - path-routes
-    - retry-errors
-    - rewrite-enabled
-    - rewrite-target
-    - service-namespace
-    - session-cookie-max-age
-    - tls-secret-name
-
-See [the config section](https://charmhub.io/nginx-ingress-integrator/configure) for descriptions
-of each, along with the required type.
-
-As an example, add the following to `src/charm.py`:
-```
-from charms.nginx_ingress_integrator.v0.ingress import IngressRequires
-
-# In your charm's `__init__` method.
-self.ingress = IngressRequires(self, {"service-hostname": self.config["external_hostname"],
-                                      "service-name": self.app.name,
-                                      "service-port": 80})
-
-# In your charm's `config-changed` handler.
-self.ingress.update_config({"service-hostname": self.config["external_hostname"]})
-```
-And then add the following to `metadata.yaml`:
-```
-requires:
-  ingress:
-    interface: ingress
-```
-You _must_ register the IngressRequires class as part of the `__init__` method
-rather than, for instance, a config-changed event handler. This is because
-doing so won't get the current relation changed event, because it wasn't
-registered to handle the event (because it wasn't created in `__init__` when
-the event was fired).
-"""
-
-import logging
-
-from ops.charm import CharmEvents
-from ops.framework import EventBase, EventSource, Object
-from ops.model import BlockedStatus
-
-# The unique Charmhub library identifier, never change it
-LIBID = "db0af4367506491c91663468fb5caa4c"
-
-# 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 = 10
-
-logger = logging.getLogger(__name__)
-
-REQUIRED_INGRESS_RELATION_FIELDS = {
-    "service-hostname",
-    "service-name",
-    "service-port",
-}
-
-OPTIONAL_INGRESS_RELATION_FIELDS = {
-    "additional-hostnames",
-    "limit-rps",
-    "limit-whitelist",
-    "max-body-size",
-    "owasp-modsecurity-crs",
-    "path-routes",
-    "retry-errors",
-    "rewrite-target",
-    "rewrite-enabled",
-    "service-namespace",
-    "session-cookie-max-age",
-    "tls-secret-name",
-}
-
-
-class IngressAvailableEvent(EventBase):
-    pass
-
-
-class IngressBrokenEvent(EventBase):
-    pass
-
-
-class IngressCharmEvents(CharmEvents):
-    """Custom charm events."""
-
-    ingress_available = EventSource(IngressAvailableEvent)
-    ingress_broken = EventSource(IngressBrokenEvent)
-
-
-class IngressRequires(Object):
-    """This class defines the functionality for the 'requires' side of the 'ingress' relation.
-
-    Hook events observed:
-        - relation-changed
-    """
-
-    def __init__(self, charm, config_dict):
-        super().__init__(charm, "ingress")
-
-        self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed)
-
-        self.config_dict = config_dict
-
-    def _config_dict_errors(self, update_only=False):
-        """Check our config dict for errors."""
-        blocked_message = "Error in ingress relation, check `juju debug-log`"
-        unknown = [
-            x
-            for x in self.config_dict
-            if x not in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
-        ]
-        if unknown:
-            logger.error(
-                "Ingress relation error, unknown key(s) in config dictionary found: %s",
-                ", ".join(unknown),
-            )
-            self.model.unit.status = BlockedStatus(blocked_message)
-            return True
-        if not update_only:
-            missing = [x for x in REQUIRED_INGRESS_RELATION_FIELDS if x not in self.config_dict]
-            if missing:
-                logger.error(
-                    "Ingress relation error, missing required key(s) in config dictionary: %s",
-                    ", ".join(sorted(missing)),
-                )
-                self.model.unit.status = BlockedStatus(blocked_message)
-                return True
-        return False
-
-    def _on_relation_changed(self, event):
-        """Handle the relation-changed event."""
-        # `self.unit` isn't available here, so use `self.model.unit`.
-        if self.model.unit.is_leader():
-            if self._config_dict_errors():
-                return
-            for key in self.config_dict:
-                event.relation.data[self.model.app][key] = str(self.config_dict[key])
-
-    def update_config(self, config_dict):
-        """Allow for updates to relation."""
-        if self.model.unit.is_leader():
-            self.config_dict = config_dict
-            if self._config_dict_errors(update_only=True):
-                return
-            relation = self.model.get_relation("ingress")
-            if relation:
-                for key in self.config_dict:
-                    relation.data[self.model.app][key] = str(self.config_dict[key])
-
-
-class IngressProvides(Object):
-    """This class defines the functionality for the 'provides' side of the 'ingress' relation.
-
-    Hook events observed:
-        - relation-changed
-    """
-
-    def __init__(self, charm):
-        super().__init__(charm, "ingress")
-        # Observe the relation-changed hook event and bind
-        # self.on_relation_changed() to handle the event.
-        self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed)
-        self.framework.observe(charm.on["ingress"].relation_broken, self._on_relation_broken)
-        self.charm = charm
-
-    def _on_relation_changed(self, event):
-        """Handle a change to the ingress relation.
-
-        Confirm we have the fields we expect to receive."""
-        # `self.unit` isn't available here, so use `self.model.unit`.
-        if not self.model.unit.is_leader():
-            return
-
-        ingress_data = {
-            field: event.relation.data[event.app].get(field)
-            for field in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
-        }
-
-        missing_fields = sorted(
-            [
-                field
-                for field in REQUIRED_INGRESS_RELATION_FIELDS
-                if ingress_data.get(field) is None
-            ]
-        )
-
-        if missing_fields:
-            logger.error(
-                "Missing required data fields for ingress relation: {}".format(
-                    ", ".join(missing_fields)
-                )
-            )
-            self.model.unit.status = BlockedStatus(
-                "Missing fields for ingress: {}".format(", ".join(missing_fields))
-            )
-
-        # Create an event that our charm can use to decide it's okay to
-        # configure the ingress.
-        self.charm.on.ingress_available.emit()
-
-    def _on_relation_broken(self, _):
-        """Handle a relation-broken event in the ingress relation."""
-        if not self.model.unit.is_leader():
-            return
-
-        # Create an event that our charm can use to remove the ingress resource.
-        self.charm.on.ingress_broken.emit()
diff --git a/installers/charm/osm-ng-ui/lib/charms/observability_libs/v1/kubernetes_service_patch.py b/installers/charm/osm-ng-ui/lib/charms/observability_libs/v1/kubernetes_service_patch.py
deleted file mode 100644 (file)
index 506dbf0..0000000
+++ /dev/null
@@ -1,291 +0,0 @@
-# Copyright 2021 Canonical Ltd.
-# See LICENSE file for licensing details.
-#   http://www.apache.org/licenses/LICENSE-2.0
-
-"""# KubernetesServicePatch Library.
-
-This library is designed to enable developers to more simply patch the Kubernetes Service created
-by Juju during the deployment of a sidecar charm. When sidecar charms are deployed, Juju creates a
-service named after the application in the namespace (named after the Juju model). This service by
-default contains a "placeholder" port, which is 65536/TCP.
-
-When modifying the default set of resources managed by Juju, one must consider the lifecycle of the
-charm. In this case, any modifications to the default service (created during deployment), will be
-overwritten during a charm upgrade.
-
-When initialised, this library binds a handler to the parent charm's `install` and `upgrade_charm`
-events which applies the patch to the cluster. This should ensure that the service ports are
-correct throughout the charm's life.
-
-The constructor simply takes a reference to the parent charm, and a list of
-[`lightkube`](https://github.com/gtsystem/lightkube) ServicePorts that each define a port for the
-service. For information regarding the `lightkube` `ServicePort` model, please visit the
-`lightkube` [docs](https://gtsystem.github.io/lightkube-models/1.23/models/core_v1/#serviceport).
-
-Optionally, a name of the service (in case service name needs to be patched as well), labels,
-selectors, and annotations can be provided as keyword arguments.
-
-## Getting Started
-
-To get started using the library, you just need to fetch the library using `charmcraft`. **Note
-that you also need to add `lightkube` and `lightkube-models` to your charm's `requirements.txt`.**
-
-```shell
-cd some-charm
-charmcraft fetch-lib charms.observability_libs.v0.kubernetes_service_patch
-echo <<-EOF >> requirements.txt
-lightkube
-lightkube-models
-EOF
-```
-
-Then, to initialise the library:
-
-For `ClusterIP` services:
-
-```python
-# ...
-from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
-from lightkube.models.core_v1 import ServicePort
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    port = ServicePort(443, name=f"{self.app.name}")
-    self.service_patcher = KubernetesServicePatch(self, [port])
-    # ...
-```
-
-For `LoadBalancer`/`NodePort` services:
-
-```python
-# ...
-from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
-from lightkube.models.core_v1 import ServicePort
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    port = ServicePort(443, name=f"{self.app.name}", targetPort=443, nodePort=30666)
-    self.service_patcher = KubernetesServicePatch(
-        self, [port], "LoadBalancer"
-    )
-    # ...
-```
-
-Port protocols can also be specified. Valid protocols are `"TCP"`, `"UDP"`, and `"SCTP"`
-
-```python
-# ...
-from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
-from lightkube.models.core_v1 import ServicePort
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    tcp = ServicePort(443, name=f"{self.app.name}-tcp", protocol="TCP")
-    udp = ServicePort(443, name=f"{self.app.name}-udp", protocol="UDP")
-    sctp = ServicePort(443, name=f"{self.app.name}-sctp", protocol="SCTP")
-    self.service_patcher = KubernetesServicePatch(self, [tcp, udp, sctp])
-    # ...
-```
-
-Additionally, you may wish to use mocks in your charm's unit testing to ensure that the library
-does not try to make any API calls, or open any files during testing that are unlikely to be
-present, and could break your tests. The easiest way to do this is during your test `setUp`:
-
-```python
-# ...
-
-@patch("charm.KubernetesServicePatch", lambda x, y: None)
-def setUp(self, *unused):
-    self.harness = Harness(SomeCharm)
-    # ...
-```
-"""
-
-import logging
-from types import MethodType
-from typing import List, Literal
-
-from lightkube import ApiError, Client
-from lightkube.models.core_v1 import ServicePort, ServiceSpec
-from lightkube.models.meta_v1 import ObjectMeta
-from lightkube.resources.core_v1 import Service
-from lightkube.types import PatchType
-from ops.charm import CharmBase
-from ops.framework import Object
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0042f86d0a874435adef581806cddbbb"
-
-# 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 = 1
-
-ServiceType = Literal["ClusterIP", "LoadBalancer"]
-
-
-class KubernetesServicePatch(Object):
-    """A utility for patching the Kubernetes service set up by Juju."""
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        ports: List[ServicePort],
-        service_name: str = None,
-        service_type: ServiceType = "ClusterIP",
-        additional_labels: dict = None,
-        additional_selectors: dict = None,
-        additional_annotations: dict = None,
-    ):
-        """Constructor for KubernetesServicePatch.
-
-        Args:
-            charm: the charm that is instantiating the library.
-            ports: a list of ServicePorts
-            service_name: allows setting custom name to the patched service. If none given,
-                application name will be used.
-            service_type: desired type of K8s service. Default value is in line with ServiceSpec's
-                default value.
-            additional_labels: Labels to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_selectors: Selectors to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_annotations: Annotations to be added to the kubernetes service.
-        """
-        super().__init__(charm, "kubernetes-service-patch")
-        self.charm = charm
-        self.service_name = service_name if service_name else self._app
-        self.service = self._service_object(
-            ports,
-            service_name,
-            service_type,
-            additional_labels,
-            additional_selectors,
-            additional_annotations,
-        )
-
-        # Make mypy type checking happy that self._patch is a method
-        assert isinstance(self._patch, MethodType)
-        # Ensure this patch is applied during the 'install' and 'upgrade-charm' events
-        self.framework.observe(charm.on.install, self._patch)
-        self.framework.observe(charm.on.upgrade_charm, self._patch)
-
-    def _service_object(
-        self,
-        ports: List[ServicePort],
-        service_name: str = None,
-        service_type: ServiceType = "ClusterIP",
-        additional_labels: dict = None,
-        additional_selectors: dict = None,
-        additional_annotations: dict = None,
-    ) -> Service:
-        """Creates a valid Service representation.
-
-        Args:
-            ports: a list of ServicePorts
-            service_name: allows setting custom name to the patched service. If none given,
-                application name will be used.
-            service_type: desired type of K8s service. Default value is in line with ServiceSpec's
-                default value.
-            additional_labels: Labels to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_selectors: Selectors to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_annotations: Annotations to be added to the kubernetes service.
-
-        Returns:
-            Service: A valid representation of a Kubernetes Service with the correct ports.
-        """
-        if not service_name:
-            service_name = self._app
-        labels = {"app.kubernetes.io/name": self._app}
-        if additional_labels:
-            labels.update(additional_labels)
-        selector = {"app.kubernetes.io/name": self._app}
-        if additional_selectors:
-            selector.update(additional_selectors)
-        return Service(
-            apiVersion="v1",
-            kind="Service",
-            metadata=ObjectMeta(
-                namespace=self._namespace,
-                name=service_name,
-                labels=labels,
-                annotations=additional_annotations,  # type: ignore[arg-type]
-            ),
-            spec=ServiceSpec(
-                selector=selector,
-                ports=ports,
-                type=service_type,
-            ),
-        )
-
-    def _patch(self, _) -> None:
-        """Patch the Kubernetes service created by Juju to map the correct port.
-
-        Raises:
-            PatchFailed: if patching fails due to lack of permissions, or otherwise.
-        """
-        if not self.charm.unit.is_leader():
-            return
-
-        client = Client()
-        try:
-            if self.service_name != self._app:
-                self._delete_and_create_service(client)
-            client.patch(Service, self.service_name, self.service, patch_type=PatchType.MERGE)
-        except ApiError as e:
-            if e.status.code == 403:
-                logger.error("Kubernetes service patch failed: `juju trust` this application.")
-            else:
-                logger.error("Kubernetes service patch failed: %s", str(e))
-        else:
-            logger.info("Kubernetes service '%s' patched successfully", self._app)
-
-    def _delete_and_create_service(self, client: Client):
-        service = client.get(Service, self._app, namespace=self._namespace)
-        service.metadata.name = self.service_name  # type: ignore[attr-defined]
-        service.metadata.resourceVersion = service.metadata.uid = None  # type: ignore[attr-defined]   # noqa: E501
-        client.delete(Service, self._app, namespace=self._namespace)
-        client.create(service)
-
-    def is_patched(self) -> bool:
-        """Reports if the service patch has been applied.
-
-        Returns:
-            bool: A boolean indicating if the service patch has been applied.
-        """
-        client = Client()
-        # Get the relevant service from the cluster
-        service = client.get(Service, name=self.service_name, namespace=self._namespace)
-        # Construct a list of expected ports, should the patch be applied
-        expected_ports = [(p.port, p.targetPort) for p in self.service.spec.ports]
-        # Construct a list in the same manner, using the fetched service
-        fetched_ports = [(p.port, p.targetPort) for p in service.spec.ports]  # type: ignore[attr-defined]  # noqa: E501
-        return expected_ports == fetched_ports
-
-    @property
-    def _app(self) -> str:
-        """Name of the current Juju application.
-
-        Returns:
-            str: A string containing the name of the current Juju application.
-        """
-        return self.charm.app.name
-
-    @property
-    def _namespace(self) -> str:
-        """The Kubernetes namespace we're running in.
-
-        Returns:
-            str: A string containing the name of the current Kubernetes namespace.
-        """
-        with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") as f:
-            return f.read().strip()
diff --git a/installers/charm/osm-ng-ui/lib/charms/osm_libs/v0/utils.py b/installers/charm/osm-ng-ui/lib/charms/osm_libs/v0/utils.py
deleted file mode 100644 (file)
index d739ba6..0000000
+++ /dev/null
@@ -1,544 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-#         http://www.apache.org/licenses/LICENSE-2.0
-"""OSM Utils Library.
-
-This library offers some utilities made for but not limited to Charmed OSM.
-
-# Getting started
-
-Execute the following command inside your Charmed Operator folder to fetch the library.
-
-```shell
-charmcraft fetch-lib charms.osm_libs.v0.utils
-```
-
-# CharmError Exception
-
-An exception that takes to arguments, the message and the StatusBase class, which are useful
-to set the status of the charm when the exception raises.
-
-Example:
-```shell
-from charms.osm_libs.v0.utils import CharmError
-
-class MyCharm(CharmBase):
-    def _on_config_changed(self, _):
-        try:
-            if not self.config.get("some-option"):
-                raise CharmError("need some-option", BlockedStatus)
-
-            if not self.mysql_ready:
-                raise CharmError("waiting for mysql", WaitingStatus)
-
-            # Do stuff...
-
-        exception CharmError as e:
-            self.unit.status = e.status
-```
-
-# Pebble validations
-
-The `check_container_ready` function checks that a container is ready,
-and therefore Pebble is ready.
-
-The `check_service_active` function checks that a service in a container is running.
-
-Both functions raise a CharmError if the validations fail.
-
-Example:
-```shell
-from charms.osm_libs.v0.utils import check_container_ready, check_service_active
-
-class MyCharm(CharmBase):
-    def _on_config_changed(self, _):
-        try:
-            container: Container = self.unit.get_container("my-container")
-            check_container_ready(container)
-            check_service_active(container, "my-service")
-            # Do stuff...
-
-        exception CharmError as e:
-            self.unit.status = e.status
-```
-
-# Debug-mode
-
-The debug-mode allows OSM developers to easily debug OSM modules.
-
-Example:
-```shell
-from charms.osm_libs.v0.utils import DebugMode
-
-class MyCharm(CharmBase):
-    _stored = StoredState()
-
-    def __init__(self, _):
-        # ...
-        container: Container = self.unit.get_container("my-container")
-        hostpaths = [
-            HostPath(
-                config="module-hostpath",
-                container_path="/usr/lib/python3/dist-packages/module"
-            ),
-        ]
-        vscode_workspace_path = "files/vscode-workspace.json"
-        self.debug_mode = DebugMode(
-            self,
-            self._stored,
-            container,
-            hostpaths,
-            vscode_workspace_path,
-        )
-
-    def _on_update_status(self, _):
-        if self.debug_mode.started:
-            return
-        # ...
-
-    def _get_debug_mode_information(self):
-        command = self.debug_mode.command
-        password = self.debug_mode.password
-        return command, password
-```
-
-# More
-
-- Get pod IP with `get_pod_ip()`
-"""
-from dataclasses import dataclass
-import logging
-import secrets
-import socket
-from pathlib import Path
-from typing import List
-
-from lightkube import Client
-from lightkube.models.core_v1 import HostPathVolumeSource, Volume, VolumeMount
-from lightkube.resources.apps_v1 import StatefulSet
-from ops.charm import CharmBase
-from ops.framework import Object, StoredState
-from ops.model import (
-    ActiveStatus,
-    BlockedStatus,
-    Container,
-    MaintenanceStatus,
-    StatusBase,
-    WaitingStatus,
-)
-from ops.pebble import ServiceStatus
-
-# The unique Charmhub library identifier, never change it
-LIBID = "e915908eebee4cdd972d484728adf984"
-
-# 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 = 5
-
-logger = logging.getLogger(__name__)
-
-
-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)
-
-
-def check_container_ready(container: Container) -> None:
-    """Check Pebble has started in the container.
-
-    Args:
-        container (Container): Container to be checked.
-
-    Raises:
-        CharmError: if container is not ready.
-    """
-    if not container.can_connect():
-        raise CharmError("waiting for pebble to start", MaintenanceStatus)
-
-
-def check_service_active(container: Container, service_name: str) -> None:
-    """Check if the service is running.
-
-    Args:
-        container (Container): Container to be checked.
-        service_name (str): Name of the service to check.
-
-    Raises:
-        CharmError: if the service is not running.
-    """
-    if service_name not in container.get_plan().services:
-        raise CharmError(f"{service_name} service not configured yet", WaitingStatus)
-
-    if container.get_service(service_name).current != ServiceStatus.ACTIVE:
-        raise CharmError(f"{service_name} service is not running")
-
-
-def get_pod_ip() -> str:
-    """Get Kubernetes Pod IP.
-
-    Returns:
-        str: The IP of the Pod.
-    """
-    return socket.gethostbyname(socket.gethostname())
-
-
-_DEBUG_SCRIPT = r"""#!/bin/bash
-# Install SSH
-
-function download_code(){{
-    wget https://go.microsoft.com/fwlink/?LinkID=760868 -O code.deb
-}}
-
-function setup_envs(){{
-    grep "source /debug.envs" /root/.bashrc || echo "source /debug.envs" | tee -a /root/.bashrc
-}}
-function setup_ssh(){{
-    apt install ssh -y
-    cat /etc/ssh/sshd_config |
-        grep -E '^PermitRootLogin yes$$' || (
-        echo PermitRootLogin yes |
-        tee -a /etc/ssh/sshd_config
-    )
-    service ssh stop
-    sleep 3
-    service ssh start
-    usermod --password $(echo {} | openssl passwd -1 -stdin) root
-}}
-
-function setup_code(){{
-    apt install libasound2 -y
-    (dpkg -i code.deb || apt-get install -f -y || apt-get install -f -y) && echo Code installed successfully
-    code --install-extension ms-python.python --user-data-dir /root
-    mkdir -p /root/.vscode-server
-    cp -R /root/.vscode/extensions /root/.vscode-server/extensions
-}}
-
-export DEBIAN_FRONTEND=noninteractive
-apt update && apt install wget -y
-download_code &
-setup_ssh &
-setup_envs
-wait
-setup_code &
-wait
-"""
-
-
-@dataclass
-class SubModule:
-    """Represent RO Submodules."""
-    sub_module_path: str
-    container_path: str
-
-
-class HostPath:
-    """Represents a hostpath."""
-    def __init__(self, config: str, container_path: str, submodules: dict = None) -> None:
-        mount_path_items = config.split("-")
-        mount_path_items.reverse()
-        self.mount_path = "/" + "/".join(mount_path_items)
-        self.config = config
-        self.sub_module_dict = {}
-        if submodules:
-            for submodule in submodules.keys():
-                self.sub_module_dict[submodule] = SubModule(
-                    sub_module_path=self.mount_path + "/" + submodule + "/" + submodules[submodule].split("/")[-1],
-                    container_path=submodules[submodule],
-                )
-        else:
-            self.container_path = container_path
-            self.module_name = container_path.split("/")[-1]
-
-class DebugMode(Object):
-    """Class to handle the debug-mode."""
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        stored: StoredState,
-        container: Container,
-        hostpaths: List[HostPath] = [],
-        vscode_workspace_path: str = "files/vscode-workspace.json",
-    ) -> None:
-        super().__init__(charm, "debug-mode")
-
-        self.charm = charm
-        self._stored = stored
-        self.hostpaths = hostpaths
-        self.vscode_workspace = Path(vscode_workspace_path).read_text()
-        self.container = container
-
-        self._stored.set_default(
-            debug_mode_started=False,
-            debug_mode_vscode_command=None,
-            debug_mode_password=None,
-        )
-
-        self.framework.observe(self.charm.on.config_changed, self._on_config_changed)
-        self.framework.observe(self.charm.on[container.name].pebble_ready, self._on_config_changed)
-        self.framework.observe(self.charm.on.update_status, self._on_update_status)
-
-    def _on_config_changed(self, _) -> None:
-        """Handler for the config-changed event."""
-        if not self.charm.unit.is_leader():
-            return
-
-        debug_mode_enabled = self.charm.config.get("debug-mode", False)
-        action = self.enable if debug_mode_enabled else self.disable
-        action()
-
-    def _on_update_status(self, _) -> None:
-        """Handler for the update-status event."""
-        if not self.charm.unit.is_leader() or not self.started:
-            return
-
-        self.charm.unit.status = ActiveStatus("debug-mode: ready")
-
-    @property
-    def started(self) -> bool:
-        """Indicates whether the debug-mode has started or not."""
-        return self._stored.debug_mode_started
-
-    @property
-    def command(self) -> str:
-        """Command to launch vscode."""
-        return self._stored.debug_mode_vscode_command
-
-    @property
-    def password(self) -> str:
-        """SSH password."""
-        return self._stored.debug_mode_password
-
-    def enable(self, service_name: str = None) -> None:
-        """Enable debug-mode.
-
-        This function mounts hostpaths of the OSM modules (if set), and
-        configures the container so it can be easily debugged. The setup
-        includes the configuration of SSH, environment variables, and
-        VSCode workspace and plugins.
-
-        Args:
-            service_name (str, optional): Pebble service name which has the desired environment
-                variables. Mandatory if there is more than one Pebble service configured.
-        """
-        hostpaths_to_reconfigure = self._hostpaths_to_reconfigure()
-        if self.started and not hostpaths_to_reconfigure:
-            self.charm.unit.status = ActiveStatus("debug-mode: ready")
-            return
-
-        logger.debug("enabling debug-mode")
-
-        # Mount hostpaths if set.
-        # If hostpaths are mounted, the statefulset will be restarted,
-        # and for that reason we return immediately. On restart, the hostpaths
-        # won't be mounted and then we can continue and setup the debug-mode.
-        if hostpaths_to_reconfigure:
-            self.charm.unit.status = MaintenanceStatus("debug-mode: configuring hostpaths")
-            self._configure_hostpaths(hostpaths_to_reconfigure)
-            return
-
-        self.charm.unit.status = MaintenanceStatus("debug-mode: starting")
-        password = secrets.token_hex(8)
-        self._setup_debug_mode(
-            password,
-            service_name,
-            mounted_hostpaths=[hp for hp in self.hostpaths if self.charm.config.get(hp.config)],
-        )
-
-        self._stored.debug_mode_vscode_command = self._get_vscode_command(get_pod_ip())
-        self._stored.debug_mode_password = password
-        self._stored.debug_mode_started = True
-        logger.info("debug-mode is ready")
-        self.charm.unit.status = ActiveStatus("debug-mode: ready")
-
-    def disable(self) -> None:
-        """Disable debug-mode."""
-        logger.debug("disabling debug-mode")
-        current_status = self.charm.unit.status
-        hostpaths_unmounted = self._unmount_hostpaths()
-
-        if not self._stored.debug_mode_started:
-            return
-        self._stored.debug_mode_started = False
-        self._stored.debug_mode_vscode_command = None
-        self._stored.debug_mode_password = None
-
-        if not hostpaths_unmounted:
-            self.charm.unit.status = current_status
-            self._restart()
-
-    def _hostpaths_to_reconfigure(self) -> List[HostPath]:
-        hostpaths_to_reconfigure: List[HostPath] = []
-        client = Client()
-        statefulset = client.get(StatefulSet, self.charm.app.name, namespace=self.charm.model.name)
-        volumes = statefulset.spec.template.spec.volumes
-
-        for hostpath in self.hostpaths:
-            hostpath_is_set = True if self.charm.config.get(hostpath.config) else False
-            hostpath_already_configured = next(
-                (True for volume in volumes if volume.name == hostpath.config), False
-            )
-            if hostpath_is_set != hostpath_already_configured:
-                hostpaths_to_reconfigure.append(hostpath)
-
-        return hostpaths_to_reconfigure
-
-    def _setup_debug_mode(
-        self,
-        password: str,
-        service_name: str = None,
-        mounted_hostpaths: List[HostPath] = [],
-    ) -> None:
-        services = self.container.get_plan().services
-        if not service_name and len(services) != 1:
-            raise Exception("Cannot start debug-mode: please set the service_name")
-
-        service = None
-        if not service_name:
-            service_name, service = services.popitem()
-        if not service:
-            service = services.get(service_name)
-
-        logger.debug(f"getting environment variables from service {service_name}")
-        environment = service.environment
-        environment_file_content = "\n".join(
-            [f'export {key}="{value}"' for key, value in environment.items()]
-        )
-        logger.debug(f"pushing environment file to {self.container.name} container")
-        self.container.push("/debug.envs", environment_file_content)
-
-        # Push VSCode workspace
-        logger.debug(f"pushing vscode workspace to {self.container.name} container")
-        self.container.push("/debug.code-workspace", self.vscode_workspace)
-
-        # Execute debugging script
-        logger.debug(f"pushing debug-mode setup script to {self.container.name} container")
-        self.container.push("/debug.sh", _DEBUG_SCRIPT.format(password), permissions=0o777)
-        logger.debug(f"executing debug-mode setup script in {self.container.name} container")
-        self.container.exec(["/debug.sh"]).wait_output()
-        logger.debug(f"stopping service {service_name} in {self.container.name} container")
-        self.container.stop(service_name)
-
-        # Add symlinks to mounted hostpaths
-        for hostpath in mounted_hostpaths:
-            logger.debug(f"adding symlink for {hostpath.config}")
-            if len(hostpath.sub_module_dict) > 0:
-                for sub_module in hostpath.sub_module_dict.keys():
-                    self.container.exec(["rm", "-rf", hostpath.sub_module_dict[sub_module].container_path]).wait_output()
-                    self.container.exec(
-                        [
-                            "ln",
-                            "-s",
-                            hostpath.sub_module_dict[sub_module].sub_module_path,
-                            hostpath.sub_module_dict[sub_module].container_path,
-                        ]
-                    )
-
-            else:
-                self.container.exec(["rm", "-rf", hostpath.container_path]).wait_output()
-                self.container.exec(
-                    [
-                        "ln",
-                        "-s",
-                        f"{hostpath.mount_path}/{hostpath.module_name}",
-                        hostpath.container_path,
-                    ]
-                )
-
-    def _configure_hostpaths(self, hostpaths: List[HostPath]):
-        client = Client()
-        statefulset = client.get(StatefulSet, self.charm.app.name, namespace=self.charm.model.name)
-
-        for hostpath in hostpaths:
-            if self.charm.config.get(hostpath.config):
-                self._add_hostpath_to_statefulset(hostpath, statefulset)
-            else:
-                self._delete_hostpath_from_statefulset(hostpath, statefulset)
-
-        client.replace(statefulset)
-
-    def _unmount_hostpaths(self) -> bool:
-        client = Client()
-        hostpath_unmounted = False
-        statefulset = client.get(StatefulSet, self.charm.app.name, namespace=self.charm.model.name)
-
-        for hostpath in self.hostpaths:
-            if self._delete_hostpath_from_statefulset(hostpath, statefulset):
-                hostpath_unmounted = True
-
-        if hostpath_unmounted:
-            client.replace(statefulset)
-
-        return hostpath_unmounted
-
-    def _add_hostpath_to_statefulset(self, hostpath: HostPath, statefulset: StatefulSet):
-        # Add volume
-        logger.debug(f"adding volume {hostpath.config} to {self.charm.app.name} statefulset")
-        volume = Volume(
-            hostpath.config,
-            hostPath=HostPathVolumeSource(
-                path=self.charm.config[hostpath.config],
-                type="Directory",
-            ),
-        )
-        statefulset.spec.template.spec.volumes.append(volume)
-
-        # Add volumeMount
-        for statefulset_container in statefulset.spec.template.spec.containers:
-            if statefulset_container.name != self.container.name:
-                continue
-
-            logger.debug(
-                f"adding volumeMount {hostpath.config} to {self.container.name} container"
-            )
-            statefulset_container.volumeMounts.append(
-                VolumeMount(mountPath=hostpath.mount_path, name=hostpath.config)
-            )
-
-    def _delete_hostpath_from_statefulset(self, hostpath: HostPath, statefulset: StatefulSet):
-        hostpath_unmounted = False
-        for volume in statefulset.spec.template.spec.volumes:
-
-            if hostpath.config != volume.name:
-                continue
-
-            # Remove volumeMount
-            for statefulset_container in statefulset.spec.template.spec.containers:
-                if statefulset_container.name != self.container.name:
-                    continue
-                for volume_mount in statefulset_container.volumeMounts:
-                    if volume_mount.name != hostpath.config:
-                        continue
-
-                    logger.debug(
-                        f"removing volumeMount {hostpath.config} from {self.container.name} container"
-                    )
-                    statefulset_container.volumeMounts.remove(volume_mount)
-
-            # Remove volume
-            logger.debug(
-                f"removing volume {hostpath.config} from {self.charm.app.name} statefulset"
-            )
-            statefulset.spec.template.spec.volumes.remove(volume)
-
-            hostpath_unmounted = True
-        return hostpath_unmounted
-
-    def _get_vscode_command(
-        self,
-        pod_ip: str,
-        user: str = "root",
-        workspace_path: str = "/debug.code-workspace",
-    ) -> str:
-        return f"code --remote ssh-remote+{user}@{pod_ip} {workspace_path}"
-
-    def _restart(self):
-        self.container.exec(["kill", "-HUP", "1"])
diff --git a/installers/charm/osm-ng-ui/lib/charms/osm_nbi/v0/nbi.py b/installers/charm/osm-ng-ui/lib/charms/osm_nbi/v0/nbi.py
deleted file mode 100644 (file)
index 130b6fa..0000000
+++ /dev/null
@@ -1,178 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# Learn more at: https://juju.is/docs/sdk
-
-"""Nbi library.
-
-This [library](https://juju.is/docs/sdk/libraries) implements both sides of the
-`nbi` [interface](https://juju.is/docs/sdk/relations).
-
-The *provider* side of this interface is implemented by the
-[osm-nbi Charmed Operator](https://charmhub.io/osm-nbi).
-
-Any Charmed Operator that *requires* NBI for providing its
-service should implement the *requirer* side of this interface.
-
-In a nutshell using this library to implement a Charmed Operator *requiring*
-NBI would look like
-
-```
-$ charmcraft fetch-lib charms.osm_nbi.v0.nbi
-```
-
-`metadata.yaml`:
-
-```
-requires:
-  nbi:
-    interface: nbi
-    limit: 1
-```
-
-`src/charm.py`:
-
-```
-from charms.osm_nbi.v0.nbi import NbiRequires
-from ops.charm import CharmBase
-
-
-class MyCharm(CharmBase):
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.nbi = NbiRequires(self)
-        self.framework.observe(
-            self.on["nbi"].relation_changed,
-            self._on_nbi_relation_changed,
-        )
-        self.framework.observe(
-            self.on["nbi"].relation_broken,
-            self._on_nbi_relation_broken,
-        )
-        self.framework.observe(
-            self.on["nbi"].relation_broken,
-            self._on_nbi_broken,
-        )
-
-    def _on_nbi_relation_broken(self, event):
-        # Get NBI host and port
-        host: str = self.nbi.host
-        port: int = self.nbi.port
-        # host => "osm-nbi"
-        # port => 9999
-
-    def _on_nbi_broken(self, event):
-        # Stop service
-        # ...
-        self.unit.status = BlockedStatus("need nbi relation")
-```
-
-You can file bugs
-[here](https://osm.etsi.org/bugzilla/enter_bug.cgi), selecting the `devops` module!
-"""
-from typing import Optional
-
-from ops.charm import CharmBase, CharmEvents
-from ops.framework import EventBase, EventSource, Object
-from ops.model import Relation
-
-
-# The unique Charmhub library identifier, never change it
-LIBID = "8c888f7c869949409e12c16d78ec068b"
-
-# 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 = 1
-
-NBI_HOST_APP_KEY = "host"
-NBI_PORT_APP_KEY = "port"
-
-
-class NbiRequires(Object):  # pragma: no cover
-    """Requires-side of the Nbi relation."""
-
-    def __init__(self, charm: CharmBase, endpoint_name: str = "nbi") -> None:
-        super().__init__(charm, endpoint_name)
-        self.charm = charm
-        self._endpoint_name = endpoint_name
-
-    @property
-    def host(self) -> str:
-        """Get nbi hostname."""
-        relation: Relation = self.model.get_relation(self._endpoint_name)
-        return (
-            relation.data[relation.app].get(NBI_HOST_APP_KEY)
-            if relation and relation.app
-            else None
-        )
-
-    @property
-    def port(self) -> int:
-        """Get nbi port number."""
-        relation: Relation = self.model.get_relation(self._endpoint_name)
-        return (
-            int(relation.data[relation.app].get(NBI_PORT_APP_KEY))
-            if relation and relation.app
-            else None
-        )
-
-
-class NbiProvides(Object):
-    """Provides-side of the Nbi relation."""
-
-    def __init__(self, charm: CharmBase, endpoint_name: str = "nbi") -> None:
-        super().__init__(charm, endpoint_name)
-        self._endpoint_name = endpoint_name
-
-    def set_host_info(self, host: str, port: int, relation: Optional[Relation] = None) -> None:
-        """Set Nbi host and port.
-
-        This function writes in the application data of the relation, therefore,
-        only the unit leader can call it.
-
-        Args:
-            host (str): Nbi hostname or IP address.
-            port (int): Nbi port.
-            relation (Optional[Relation]): Relation to update.
-                                           If not specified, all relations will be updated.
-
-        Raises:
-            Exception: if a non-leader unit calls this function.
-        """
-        if not self.model.unit.is_leader():
-            raise Exception("only the leader set host information.")
-
-        if relation:
-            self._update_relation_data(host, port, relation)
-            return
-
-        for relation in self.model.relations[self._endpoint_name]:
-            self._update_relation_data(host, port, relation)
-
-    def _update_relation_data(self, host: str, port: int, relation: Relation) -> None:
-        """Update data in relation if needed."""
-        relation.data[self.model.app][NBI_HOST_APP_KEY] = host
-        relation.data[self.model.app][NBI_PORT_APP_KEY] = str(port)
diff --git a/installers/charm/osm-ng-ui/metadata.yaml b/installers/charm/osm-ng-ui/metadata.yaml
deleted file mode 100644 (file)
index be03f24..0000000
+++ /dev/null
@@ -1,57 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Overview on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
-
-name: osm-ng-ui
-
-# The following metadata are human-readable and will be published prominently on Charmhub.
-
-display-name: OSM NG-UI
-
-summary: OSM Next-generation User Interface (NG-UI)
-
-description: |
-  A Kubernetes operator that deploys the Next-generation User Interface of OSM.
-
-  This charm doesn't make sense on its own.
-  See more:
-    - https://charmhub.io/osm
-
-containers:
-  ng-ui:
-    resource: ng-ui-image
-
-# This file populates the Resources tab on Charmhub.
-
-resources:
-  ng-ui-image:
-    type: oci-image
-    description: OCI image for ng-ui
-    upstream-source: opensourcemano/ng-ui
-
-requires:
-  ingress:
-    interface: ingress
-    limit: 1
-  nbi:
-    interface: nbi
diff --git a/installers/charm/osm-ng-ui/pyproject.toml b/installers/charm/osm-ng-ui/pyproject.toml
deleted file mode 100644 (file)
index 16cf0f4..0000000
+++ /dev/null
@@ -1,52 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-
-# 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", "E501", "D107"]
-# D100, D101, D102, D103: Ignore missing docstrings in tests
-per-file-ignores = ["tests/*:D100,D101,D102,D103,D104"]
-docstring-convention = "google"
diff --git a/installers/charm/osm-ng-ui/requirements.txt b/installers/charm/osm-ng-ui/requirements.txt
deleted file mode 100644 (file)
index 761edd8..0000000
+++ /dev/null
@@ -1,23 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-ops < 2.2
-lightkube
-lightkube-models
-git+https://github.com/charmed-osm/config-validator/
diff --git a/installers/charm/osm-ng-ui/src/charm.py b/installers/charm/osm-ng-ui/src/charm.py
deleted file mode 100755 (executable)
index ca517b3..0000000
+++ /dev/null
@@ -1,226 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# Learn more at: https://juju.is/docs/sdk
-
-"""OSM NG-UI charm.
-
-See more: https://charmhub.io/osm
-"""
-
-import logging
-import re
-from typing import Any, Dict
-
-from charms.nginx_ingress_integrator.v0.ingress import IngressRequires
-from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
-from charms.osm_libs.v0.utils import (
-    CharmError,
-    check_container_ready,
-    check_service_active,
-)
-from charms.osm_nbi.v0.nbi import NbiRequires
-from lightkube.models.core_v1 import ServicePort
-from ops.charm import CharmBase
-from ops.framework import StoredState
-from ops.main import main
-from ops.model import ActiveStatus, BlockedStatus, Container
-
-SERVICE_PORT = 80
-
-logger = logging.getLogger(__name__)
-
-
-class OsmNgUiCharm(CharmBase):
-    """OSM NG-UI Kubernetes sidecar charm."""
-
-    _stored = StoredState()
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.ingress = IngressRequires(
-            self,
-            {
-                "service-hostname": self.external_hostname,
-                "service-name": self.app.name,
-                "service-port": SERVICE_PORT,
-            },
-        )
-        self._observe_charm_events()
-        self._patch_k8s_service()
-        self._stored.set_default(default_site_patched=False)
-        self.nbi = NbiRequires(self)
-        self.container: Container = self.unit.get_container("ng-ui")
-
-    @property
-    def external_hostname(self) -> str:
-        """External hostname property.
-
-        Returns:
-            str: the external hostname from config.
-                If not set, return the ClusterIP service name.
-        """
-        return self.config.get("external-hostname") or self.app.name
-
-    # ---------------------------------------------------------------------------
-    #   Handlers for Charm Events
-    # ---------------------------------------------------------------------------
-
-    def _on_config_changed(self, _) -> None:
-        """Handler for the config-changed event."""
-        try:
-            self._validate_config()
-            self._check_relations()
-            # Check if the container is ready.
-            # Eventually it will become ready after the first pebble-ready event.
-            check_container_ready(self.container)
-
-            self._configure_service(self.container)
-            self._update_ingress_config()
-            # Update charm status
-            self._on_update_status()
-        except CharmError as e:
-            logger.debug(e.message)
-            self.unit.status = e.status
-
-    def _on_update_status(self, _=None) -> None:
-        """Handler for the update-status event."""
-        try:
-            self._check_relations()
-            check_container_ready(self.container)
-            check_service_active(self.container, "ng-ui")
-            self.unit.status = ActiveStatus()
-        except CharmError as e:
-            logger.debug(e.message)
-            self.unit.status = e.status
-
-    def _on_nbi_relation_broken(self, _) -> None:
-        """Handler for the nbi relation broken event."""
-        # Check Pebble has started in the container
-        try:
-            check_container_ready(self.container)
-            check_service_active(self.container, "ng-ui")
-            self.container.stop("ng-ui")
-            self._stored.default_site_patched = False
-        except CharmError:
-            pass
-        finally:
-            self.unit.status = BlockedStatus("need nbi relation")
-
-    # ---------------------------------------------------------------------------
-    #   Validation and configuration and more
-    # ---------------------------------------------------------------------------
-
-    def _patch_k8s_service(self) -> None:
-        port = ServicePort(SERVICE_PORT, name=f"{self.app.name}")
-        self.service_patcher = KubernetesServicePatch(self, [port])
-
-    def _observe_charm_events(self) -> None:
-        event_handler_mapping = {
-            # Core lifecycle events
-            self.on.ng_ui_pebble_ready: self._on_config_changed,
-            self.on.config_changed: self._on_config_changed,
-            self.on.update_status: self._on_update_status,
-            # Relation events
-            self.on["nbi"].relation_changed: self._on_config_changed,
-            self.on["nbi"].relation_broken: self._on_nbi_relation_broken,
-        }
-        for event, handler in event_handler_mapping.items():
-            self.framework.observe(event, handler)
-
-    def _validate_config(self) -> None:
-        """Validate charm configuration.
-
-        Raises:
-            CharmError: if charm configuration is invalid.
-        """
-        logger.debug("validating charm config")
-
-    def _check_relations(self) -> None:
-        """Validate charm relations.
-
-        Raises:
-            CharmError: if charm configuration is invalid.
-        """
-        logger.debug("check for missing relations")
-
-        if not self.nbi.host or not self.nbi.port:
-            raise CharmError("need nbi relation")
-
-    def _update_ingress_config(self) -> None:
-        """Update ingress config in relation."""
-        ingress_config = {
-            "service-hostname": self.external_hostname,
-            "max-body-size": self.config["max-body-size"],
-        }
-        if "tls-secret-name" in self.config:
-            ingress_config["tls-secret-name"] = self.config["tls-secret-name"]
-        logger.debug(f"updating ingress-config: {ingress_config}")
-        self.ingress.update_config(ingress_config)
-
-    def _configure_service(self, container: Container) -> None:
-        """Add Pebble layer with the ng-ui service."""
-        logger.debug(f"configuring {self.app.name} service")
-        self._patch_default_site(container)
-        container.add_layer("ng-ui", self._get_layer(), combine=True)
-        container.replan()
-
-    def _get_layer(self) -> Dict[str, Any]:
-        """Get layer for Pebble."""
-        return {
-            "summary": "ng-ui layer",
-            "description": "pebble config layer for ng-ui",
-            "services": {
-                "ng-ui": {
-                    "override": "replace",
-                    "summary": "ng-ui service",
-                    "command": 'nginx -g "daemon off;"',
-                    "startup": "enabled",
-                }
-            },
-        }
-
-    def _patch_default_site(self, container: Container) -> None:
-        max_body_size = self.config.get("max-body-size")
-        if (
-            self._stored.default_site_patched
-            and max_body_size == self._stored.default_site_max_body_size
-        ):
-            return
-        default_site_config = container.pull("/etc/nginx/sites-available/default").read()
-        default_site_config = re.sub(
-            "client_max_body_size .*\n",
-            f"client_max_body_size {max_body_size}M;\n",
-            default_site_config,
-        )
-        default_site_config = re.sub(
-            "proxy_pass .*\n",
-            f"proxy_pass http://{self.nbi.host}:{self.nbi.port};\n",
-            default_site_config,
-        )
-        container.push("/etc/nginx/sites-available/default", default_site_config)
-        self._stored.default_site_patched = True
-        self._stored.default_site_max_body_size = max_body_size
-
-
-if __name__ == "__main__":  # pragma: no cover
-    main(OsmNgUiCharm)
diff --git a/installers/charm/osm-ng-ui/tests/integration/test_charm.py b/installers/charm/osm-ng-ui/tests/integration/test_charm.py
deleted file mode 100644 (file)
index 3f87078..0000000
+++ /dev/null
@@ -1,157 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2023 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-# Learn more about testing at: https://juju.is/docs/sdk/testing
-
-import asyncio
-import logging
-import shlex
-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())
-NG_UI_APP = METADATA["name"]
-
-# Required charms (needed by NG UI)
-NBI_CHARM = "osm-nbi"
-NBI_APP = "nbi"
-KAFKA_CHARM = "kafka-k8s"
-KAFKA_APP = "kafka"
-MONGO_DB_CHARM = "mongodb-k8s"
-MONGO_DB_APP = "mongodb"
-PROMETHEUS_CHARM = "osm-prometheus"
-PROMETHEUS_APP = "prometheus"
-KEYSTONE_CHARM = "osm-keystone"
-KEYSTONE_APP = "keystone"
-MYSQL_CHARM = "charmed-osm-mariadb-k8s"
-MYSQL_APP = "mysql"
-ZOOKEEPER_CHARM = "zookeeper-k8s"
-ZOOKEEPER_APP = "zookeeper"
-
-INGRESS_CHARM = "nginx-ingress-integrator"
-INGRESS_APP = "ingress"
-
-ALL_APPS = [
-    NBI_APP,
-    NG_UI_APP,
-    KAFKA_APP,
-    MONGO_DB_APP,
-    PROMETHEUS_APP,
-    KEYSTONE_APP,
-    MYSQL_APP,
-    ZOOKEEPER_APP,
-]
-
-
-@pytest.mark.abort_on_fail
-async def test_ng_ui_is_deployed(ops_test: OpsTest):
-    ng_ui_charm = await ops_test.build_charm(".")
-    ng_ui_resources = {"ng-ui-image": METADATA["resources"]["ng-ui-image"]["upstream-source"]}
-    keystone_image = "opensourcemano/keystone:testing-daily"
-    keystone_deploy_cmd = f"juju deploy -m {ops_test.model_full_name} {KEYSTONE_CHARM} {KEYSTONE_APP} --resource keystone-image={keystone_image} --channel=latest/beta --series jammy"
-
-    await asyncio.gather(
-        ops_test.model.deploy(
-            ng_ui_charm, resources=ng_ui_resources, application_name=NG_UI_APP, series="jammy"
-        ),
-        ops_test.model.deploy(
-            NBI_CHARM, application_name=NBI_APP, channel="latest/beta", series="jammy"
-        ),
-        ops_test.model.deploy(KAFKA_CHARM, application_name=KAFKA_APP, channel="stable"),
-        ops_test.model.deploy(MONGO_DB_CHARM, application_name=MONGO_DB_APP, channel="5/edge"),
-        ops_test.model.deploy(PROMETHEUS_CHARM, application_name=PROMETHEUS_APP, channel="stable"),
-        ops_test.model.deploy(ZOOKEEPER_CHARM, application_name=ZOOKEEPER_APP, channel="stable"),
-        ops_test.model.deploy(MYSQL_CHARM, application_name=MYSQL_APP, channel="stable"),
-        # Keystone is deployed separately because the juju python library has a bug where resources
-        # are not properly deployed. See https://github.com/juju/python-libjuju/issues/766
-        ops_test.run(*shlex.split(keystone_deploy_cmd), check=True),
-    )
-
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(apps=ALL_APPS, timeout=300)
-    logger.info("Adding relations for other components")
-    await asyncio.gather(
-        ops_test.model.relate(MYSQL_APP, KEYSTONE_APP),
-        ops_test.model.relate(KAFKA_APP, ZOOKEEPER_APP),
-        ops_test.model.relate(KEYSTONE_APP, NBI_APP),
-        ops_test.model.relate(KAFKA_APP, NBI_APP),
-        ops_test.model.relate("{}:mongodb".format(NBI_APP), "{}:database".format(MONGO_DB_APP)),
-        ops_test.model.relate(PROMETHEUS_APP, NBI_APP),
-    )
-
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(apps=ALL_APPS, timeout=300)
-
-    assert ops_test.model.applications[NG_UI_APP].status == "blocked"
-    unit = ops_test.model.applications[NG_UI_APP].units[0]
-    assert unit.workload_status_message == "need nbi relation"
-
-    logger.info("Adding relations for NG-UI")
-    await ops_test.model.relate(NG_UI_APP, NBI_APP)
-
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(apps=ALL_APPS, status="active", timeout=300)
-
-
-@pytest.mark.abort_on_fail
-async def test_ng_ui_scales_up(ops_test: OpsTest):
-    logger.info("Scaling up osm-ng-ui")
-    expected_units = 3
-    assert len(ops_test.model.applications[NG_UI_APP].units) == 1
-    await ops_test.model.applications[NG_UI_APP].scale(expected_units)
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=[NG_UI_APP], status="active", wait_for_exact_units=expected_units
-        )
-
-
-@pytest.mark.abort_on_fail
-async def test_ng_ui_blocks_without_relation(ops_test: OpsTest):
-    await asyncio.gather(ops_test.model.applications[NBI_APP].remove_relation(NBI_APP, NG_UI_APP))
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(apps=[NG_UI_APP])
-    assert ops_test.model.applications[NG_UI_APP].status == "blocked"
-    for unit in ops_test.model.applications[NG_UI_APP].units:
-        assert unit.workload_status_message == "need nbi relation"
-    await ops_test.model.relate(NG_UI_APP, NBI_APP)
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(apps=ALL_APPS, status="active")
-
-
-@pytest.mark.abort_on_fail
-async def test_ng_ui_integration_ingress(ops_test: OpsTest):
-    # Temporal workaround due to python-libjuju 2.9.42.2 bug fixed in
-    # https://github.com/juju/python-libjuju/pull/854
-    # To be replaced when juju version 2.9.43 is used.
-    cmd = f"juju deploy {INGRESS_CHARM} {INGRESS_APP} --channel stable"
-    await ops_test.run(*shlex.split(cmd), check=True)
-
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(apps=ALL_APPS + [INGRESS_APP])
-
-    await ops_test.model.relate(NG_UI_APP, INGRESS_APP)
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(apps=ALL_APPS + [INGRESS_APP], status="active")
diff --git a/installers/charm/osm-ng-ui/tests/unit/test_charm.py b/installers/charm/osm-ng-ui/tests/unit/test_charm.py
deleted file mode 100644 (file)
index f4d4571..0000000
+++ /dev/null
@@ -1,94 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-# Learn more about testing at: https://juju.is/docs/sdk/testing
-
-import pytest
-from ops.model import ActiveStatus, BlockedStatus
-from ops.testing import Harness
-from pytest_mock import MockerFixture
-
-from charm import CharmError, OsmNgUiCharm, check_service_active
-
-container_name = "ng-ui"
-service_name = "ng-ui"
-
-sites_default = """
-server {
-    listen       80;
-    server_name  localhost;
-    root   /usr/share/nginx/html;
-    index  index.html index.htm;
-    client_max_body_size 50M;
-
-    location /osm {
-        proxy_pass https://nbi:9999;
-        proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
-        proxy_set_header Accept-Encoding "";
-    }
-
-    location / {
-        try_files $uri $uri/ /index.html;
-    }
-}
-"""
-
-
-@pytest.fixture
-def harness(mocker: MockerFixture):
-    mocker.patch("charm.KubernetesServicePatch", lambda x, y: None)
-    harness = Harness(OsmNgUiCharm)
-    harness.begin()
-    container = harness.charm.unit.get_container("ng-ui")
-    harness.set_can_connect(container, True)
-    container.push("/etc/nginx/sites-available/default", sites_default, make_dirs=True)
-    yield harness
-    harness.cleanup()
-
-
-def test_missing_relations(harness: Harness):
-    harness.charm.on.config_changed.emit()
-    assert type(harness.charm.unit.status) == BlockedStatus
-    assert harness.charm.unit.status.message == "need nbi relation"
-
-
-def test_ready(harness: Harness):
-    _add_nbi_relation(harness)
-    assert harness.charm.unit.status == ActiveStatus()
-
-
-def test_container_stops_after_relation_broken(harness: Harness):
-    harness.charm.on[container_name].pebble_ready.emit(container_name)
-    container = harness.charm.unit.get_container(container_name)
-    relation_id = _add_nbi_relation(harness)
-    check_service_active(container, service_name)
-    harness.remove_relation(relation_id)
-    with pytest.raises(CharmError):
-        check_service_active(container, service_name)
-    assert type(harness.charm.unit.status) == BlockedStatus
-    assert harness.charm.unit.status.message == "need nbi relation"
-
-
-def _add_nbi_relation(harness: Harness):
-    relation_id = harness.add_relation("nbi", "nbi")
-    harness.add_relation_unit(relation_id, "nbi/0")
-    harness.update_relation_data(relation_id, "nbi", {"host": "nbi", "port": "9999"})
-    return relation_id
diff --git a/installers/charm/osm-ng-ui/tox.ini b/installers/charm/osm-ng-ui/tox.ini
deleted file mode 100644 (file)
index 8c614b8..0000000
+++ /dev/null
@@ -1,93 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-
-[tox]
-skipsdist=True
-skip_missing_interpreters = True
-envlist = lint, unit, integration
-
-[vars]
-src_path = {toxinidir}/src
-tst_path = {toxinidir}/tests
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3.8
-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-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    # uncomment the following line if this charm owns a lib
-    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
-    # 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} \
-        -m pytest {[vars]tst_path}/unit -v --tb native -s {posargs}
-    coverage report
-    coverage xml
-
-[testenv:integration]
-description = Run integration tests
-deps =
-    juju<3.0.0
-    pytest
-    pytest-operator
-    -r{toxinidir}/requirements.txt
-commands =
-    pytest -v --tb native {[vars]tst_path}/integration --log-cli-level=INFO -s {posargs} --cloud microk8s
diff --git a/installers/charm/osm-pol/.gitignore b/installers/charm/osm-pol/.gitignore
deleted file mode 100644 (file)
index 87d0a58..0000000
+++ /dev/null
@@ -1,29 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-venv/
-build/
-*.charm
-.tox/
-.coverage
-coverage.xml
-__pycache__/
-*.py[cod]
-.vscode
\ No newline at end of file
diff --git a/installers/charm/osm-pol/.jujuignore b/installers/charm/osm-pol/.jujuignore
deleted file mode 100644 (file)
index 17c7a8b..0000000
+++ /dev/null
@@ -1,23 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-/venv
-*.py[cod]
-*.charm
diff --git a/installers/charm/osm-pol/CONTRIBUTING.md b/installers/charm/osm-pol/CONTRIBUTING.md
deleted file mode 100644 (file)
index 4bbbeea..0000000
+++ /dev/null
@@ -1,78 +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.
-
-For those usages not covered by the Apache License, Version 2.0 please
-contact: legal@canonical.com
-
-To get in touch with the maintainers, please contact:
-osm-charmers@lists.launchpad.net -->
-
-# Contributing
-
-## Overview
-
-This documents explains the processes and practices recommended for contributing enhancements to
-this operator.
-
-- Generally, before developing enhancements to this charm, you should consider [opening an issue
-  ](https://osm.etsi.org/bugzilla/enter_bug.cgi?product=OSM) explaining your use case. (Component=devops, version=master)
-- If you would like to chat with us about your use-cases or proposed implementation, you can reach
-  us at [OSM Juju public channel](https://opensourcemano.slack.com/archives/C027KJGPECA).
-- 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 gerrit patch onto
-  the `master` branch.
-
-## 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 dev
-# Enable DEBUG logging
-juju model-config logging-config="<root>=INFO;unit=DEBUG"
-# Deploy the charm
-juju deploy ./osm-pol_ubuntu-22.04-amd64.charm \
-    --resource pol-image=opensourcemano/pol:testing-daily --series jammy
-```
diff --git a/installers/charm/osm-pol/LICENSE b/installers/charm/osm-pol/LICENSE
deleted file mode 100644 (file)
index 7e9d504..0000000
+++ /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 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.
diff --git a/installers/charm/osm-pol/README.md b/installers/charm/osm-pol/README.md
deleted file mode 100644 (file)
index cd96c75..0000000
+++ /dev/null
@@ -1,43 +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.
-
-For those usages not covered by the Apache License, Version 2.0 please
-contact: legal@canonical.com
-
-To get in touch with the maintainers, please contact:
-osm-charmers@lists.launchpad.net -->
-
-<!-- 
-Avoid using this README file for information that is maintained or published elsewhere, e.g.:
-
-* metadata.yaml > published on Charmhub
-* documentation > published on (or linked to from) Charmhub
-* detailed contribution guide > documentation or CONTRIBUTING.md
-
-Use links instead. 
--->
-
-# OSM POL
-
-Charmhub package name: osm-pol
-More information: https://charmhub.io/osm-pol
-
-## Other resources
-
-* [Read more](https://osm.etsi.org/docs/user-guide/latest/) 
-
-* [Contributing](https://osm.etsi.org/gitweb/?p=osm/devops.git;a=blob;f=installers/charm/osm-pol/CONTRIBUTING.md)
-
-* See the [Juju SDK documentation](https://juju.is/docs/sdk) for more information about developing and improving charms.
-                                                           
diff --git a/installers/charm/osm-pol/actions.yaml b/installers/charm/osm-pol/actions.yaml
deleted file mode 100644 (file)
index 0d73468..0000000
+++ /dev/null
@@ -1,26 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Actions tab on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
-
-get-debug-mode-information:
-  description: Get information to debug the container
diff --git a/installers/charm/osm-pol/charmcraft.yaml b/installers/charm/osm-pol/charmcraft.yaml
deleted file mode 100644 (file)
index f5e3ff3..0000000
+++ /dev/null
@@ -1,36 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-
-type: charm
-bases:
-  - build-on:
-      - name: "ubuntu"
-        channel: "22.04"
-    run-on:
-      - name: "ubuntu"
-        channel: "22.04"
-
-parts:
-  charm:
-    # build-packages:
-    #   - git
-    prime:
-      - files/*
diff --git a/installers/charm/osm-pol/config.yaml b/installers/charm/osm-pol/config.yaml
deleted file mode 100644 (file)
index a92100d..0000000
+++ /dev/null
@@ -1,91 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Configure tab on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
-
-options:
-  log-level:
-    default: "INFO"
-    description: |
-      Set the Logging Level.
-
-      Options:
-        - TRACE
-        - DEBUG
-        - INFO
-        - WARN
-        - ERROR
-        - FATAL
-    type: string
-  mysql-uri:
-    type: string
-    description: |
-      Mysql URI with the following format:
-        mysql://<user>:<password>@<mysql_host>:<mysql_port>/<database>
-
-      This should be removed after the mysql-integrator charm is made.
-
-      If provided, this config will override the mysql relation.
-
-  # Debug-mode options
-  debug-mode:
-    type: boolean
-    description: |
-      Great for OSM Developers! (Not recommended for production deployments)
-
-      This action activates the Debug Mode, which sets up the container to be ready for debugging.
-      As part of the setup, SSH is enabled and a VSCode workspace file is automatically populated.
-
-      After enabling the debug-mode, execute the following command to get the information you need
-      to start debugging:
-        `juju run-action <unit name> get-debug-mode-information --wait`
-
-      The previous command returns the command you need to execute, and the SSH password that was set.
-
-      See also:
-        - https://charmhub.io/osm-pol/configure#pol-hostpath
-        - https://charmhub.io/osm-pol/configure#common-hostpath
-    default: false
-
-  pol-hostpath:
-    type: string
-    description: |
-      Set this config to the local path of the POL module to persist the changes done during the
-      debug-mode session.
-
-      Example:
-        $ git clone "https://osm.etsi.org/gerrit/osm/POL" /home/ubuntu/POL
-        $ juju config pol pol-hostpath=/home/ubuntu/POL
-
-      This configuration only applies if option `debug-mode` is set to true.
-
-  common-hostpath:
-    type: string
-    description: |
-      Set this config to the local path of the common module to persist the changes done during the
-      debug-mode session.
-
-      Example:
-        $ git clone "https://osm.etsi.org/gerrit/osm/common" /home/ubuntu/common
-        $ juju config pol common-hostpath=/home/ubuntu/common
-
-      This configuration only applies if option `debug-mode` is set to true.
diff --git a/installers/charm/osm-pol/files/vscode-workspace.json b/installers/charm/osm-pol/files/vscode-workspace.json
deleted file mode 100644 (file)
index 36e7c4d..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-{
-    "folders": [
-        {"path": "/usr/lib/python3/dist-packages/osm_policy_module"},
-        {"path": "/usr/lib/python3/dist-packages/osm_common"},
-    ],
-    "settings": {},
-    "launch": {
-        "version": "0.2.0",
-        "configurations": [
-            {
-                "name": "POL",
-                "type": "python",
-                "request": "launch",
-                "module": "osm_policy_module.cmd.policy_module_agent",
-                "justMyCode": false,
-            }
-        ]
-    }
-}
\ No newline at end of file
diff --git a/installers/charm/osm-pol/lib/charms/data_platform_libs/v0/data_interfaces.py b/installers/charm/osm-pol/lib/charms/data_platform_libs/v0/data_interfaces.py
deleted file mode 100644 (file)
index b3da5aa..0000000
+++ /dev/null
@@ -1,1130 +0,0 @@
-# Copyright 2023 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.
-
-"""Library to manage the relation for the data-platform products.
-
-This library contains the Requires and Provides classes for handling the relation
-between an application and multiple managed application supported by the data-team:
-MySQL, Postgresql, MongoDB, Redis,  and Kakfa.
-
-### Database (MySQL, Postgresql, MongoDB, and Redis)
-
-#### Requires Charm
-This library is a uniform interface to a selection of common database
-metadata, with added custom events that add convenience to database management,
-and methods to consume the application related data.
-
-
-Following an example of using the DatabaseCreatedEvent, in the context of the
-application charm code:
-
-```python
-
-from charms.data_platform_libs.v0.data_interfaces import (
-    DatabaseCreatedEvent,
-    DatabaseRequires,
-)
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Charm events defined in the database requires charm library.
-        self.database = DatabaseRequires(self, relation_name="database", database_name="database")
-        self.framework.observe(self.database.on.database_created, self._on_database_created)
-
-    def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-
-        # Start application with rendered configuration
-        self._start_application(config_file)
-
-        # Set active status
-        self.unit.status = ActiveStatus("received database credentials")
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
--  database_created: event emitted when the requested database is created.
--  endpoints_changed: event emitted when the read/write endpoints of the database have changed.
--  read_only_endpoints_changed: event emitted when the read-only endpoints of the database
-  have changed. Event is not triggered if read/write endpoints changed too.
-
-If it is needed to connect multiple database clusters to the same relation endpoint
-the application charm can implement the same code as if it would connect to only
-one database cluster (like the above code example).
-
-To differentiate multiple clusters connected to the same relation endpoint
-the application charm can use the name of the remote application:
-
-```python
-
-def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-    # Get the remote app name of the cluster that triggered this event
-    cluster = event.relation.app.name
-```
-
-It is also possible to provide an alias for each different database cluster/relation.
-
-So, it is possible to differentiate the clusters in two ways.
-The first is to use the remote application name, i.e., `event.relation.app.name`, as above.
-
-The second way is to use different event handlers to handle each cluster events.
-The implementation would be something like the following code:
-
-```python
-
-from charms.data_platform_libs.v0.data_interfaces import (
-    DatabaseCreatedEvent,
-    DatabaseRequires,
-)
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Define the cluster aliases and one handler for each cluster database created event.
-        self.database = DatabaseRequires(
-            self,
-            relation_name="database",
-            database_name="database",
-            relations_aliases = ["cluster1", "cluster2"],
-        )
-        self.framework.observe(
-            self.database.on.cluster1_database_created, self._on_cluster1_database_created
-        )
-        self.framework.observe(
-            self.database.on.cluster2_database_created, self._on_cluster2_database_created
-        )
-
-    def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster1
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-    def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster2
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-```
-
-### Provider Charm
-
-Following an example of using the DatabaseRequestedEvent, in the context of the
-database charm code:
-
-```python
-from charms.data_platform_libs.v0.data_interfaces import DatabaseProvides
-
-class SampleCharm(CharmBase):
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        # Charm events defined in the database provides charm library.
-        self.provided_database = DatabaseProvides(self, relation_name="database")
-        self.framework.observe(self.provided_database.on.database_requested,
-            self._on_database_requested)
-        # Database generic helper
-        self.database = DatabaseHelper()
-
-    def _on_database_requested(self, event: DatabaseRequestedEvent) -> None:
-        # Handle the event triggered by a new database requested in the relation
-        # Retrieve the database name using the charm library.
-        db_name = event.database
-        # generate a new user credential
-        username = self.database.generate_user()
-        password = self.database.generate_password()
-        # set the credentials for the relation
-        self.provided_database.set_credentials(event.relation.id, username, password)
-        # set other variables for the relation event.set_tls("False")
-```
-As shown above, the library provides a custom event (database_requested) to handle
-the situation when an application charm requests a new database to be created.
-It's preferred to subscribe to this event instead of relation changed event to avoid
-creating a new database when other information other than a database name is
-exchanged in the relation databag.
-
-### Kafka
-
-This library is the interface to use and interact with the Kafka charm. This library contains
-custom events that add convenience to manage Kafka, and provides methods to consume the
-application related data.
-
-#### Requirer Charm
-
-```python
-
-from charms.data_platform_libs.v0.data_interfaces import (
-    BootstrapServerChangedEvent,
-    KafkaRequires,
-    TopicCreatedEvent,
-)
-
-class ApplicationCharm(CharmBase):
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.kafka = KafkaRequires(self, "kafka_client", "test-topic")
-        self.framework.observe(
-            self.kafka.on.bootstrap_server_changed, self._on_kafka_bootstrap_server_changed
-        )
-        self.framework.observe(
-            self.kafka.on.topic_created, self._on_kafka_topic_created
-        )
-
-    def _on_kafka_bootstrap_server_changed(self, event: BootstrapServerChangedEvent):
-        # Event triggered when a bootstrap server was changed for this application
-
-        new_bootstrap_server = event.bootstrap_server
-        ...
-
-    def _on_kafka_topic_created(self, event: TopicCreatedEvent):
-        # Event triggered when a topic was created for this application
-        username = event.username
-        password = event.password
-        tls = event.tls
-        tls_ca= event.tls_ca
-        bootstrap_server event.bootstrap_server
-        consumer_group_prefic = event.consumer_group_prefix
-        zookeeper_uris = event.zookeeper_uris
-        ...
-
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
-- topic_created: event emitted when the requested topic is created.
-- bootstrap_server_changed: event emitted when the bootstrap server have changed.
-- credential_changed: event emitted when the credentials of Kafka changed.
-
-### Provider Charm
-
-Following the previous example, this is an example of the provider charm.
-
-```python
-class SampleCharm(CharmBase):
-
-from charms.data_platform_libs.v0.data_interfaces import (
-    KafkaProvides,
-    TopicRequestedEvent,
-)
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Default charm events.
-        self.framework.observe(self.on.start, self._on_start)
-
-        # Charm events defined in the Kafka Provides charm library.
-        self.kafka_provider = KafkaProvides(self, relation_name="kafka_client")
-        self.framework.observe(self.kafka_provider.on.topic_requested, self._on_topic_requested)
-        # Kafka generic helper
-        self.kafka = KafkaHelper()
-
-    def _on_topic_requested(self, event: TopicRequestedEvent):
-        # Handle the on_topic_requested event.
-
-        topic = event.topic
-        relation_id = event.relation.id
-        # set connection info in the databag relation
-        self.kafka_provider.set_bootstrap_server(relation_id, self.kafka.get_bootstrap_server())
-        self.kafka_provider.set_credentials(relation_id, username=username, password=password)
-        self.kafka_provider.set_consumer_group_prefix(relation_id, ...)
-        self.kafka_provider.set_tls(relation_id, "False")
-        self.kafka_provider.set_zookeeper_uris(relation_id, ...)
-
-```
-As shown above, the library provides a custom event (topic_requested) to handle
-the situation when an application charm requests a new topic to be created.
-It is preferred to subscribe to this event instead of relation changed event to avoid
-creating a new topic when other information other than a topic name is
-exchanged in the relation databag.
-"""
-
-import json
-import logging
-from abc import ABC, abstractmethod
-from collections import namedtuple
-from datetime import datetime
-from typing import List, Optional
-
-from ops.charm import (
-    CharmBase,
-    CharmEvents,
-    RelationChangedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-)
-from ops.framework import EventSource, Object
-from ops.model import Relation
-
-# The unique Charmhub library identifier, never change it
-LIBID = "6c3e6b6680d64e9c89e611d1a15f65be"
-
-# 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 = 7
-
-PYDEPS = ["ops>=2.0.0"]
-
-logger = logging.getLogger(__name__)
-
-Diff = namedtuple("Diff", "added changed deleted")
-Diff.__doc__ = """
-A tuple for storing the diff between two data mappings.
-
-added - keys that were added
-changed - keys that still exist but have new values
-deleted - key that were deleted"""
-
-
-def diff(event: RelationChangedEvent, bucket: str) -> Diff:
-    """Retrieves the diff of the data in the relation changed databag.
-
-    Args:
-        event: relation changed event.
-        bucket: bucket of the databag (app or unit)
-
-    Returns:
-        a Diff instance containing the added, deleted and changed
-            keys from the event relation databag.
-    """
-    # Retrieve the old data from the data key in the application relation databag.
-    old_data = json.loads(event.relation.data[bucket].get("data", "{}"))
-    # Retrieve the new data from the event relation databag.
-    new_data = {
-        key: value for key, value in event.relation.data[event.app].items() if key != "data"
-    }
-
-    # These are the keys that were added to the databag and triggered this event.
-    added = new_data.keys() - old_data.keys()
-    # These are the keys that were removed from the databag and triggered this event.
-    deleted = old_data.keys() - new_data.keys()
-    # These are the keys that already existed in the databag,
-    # but had their values changed.
-    changed = {key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]}
-    # Convert the new_data to a serializable format and save it for a next diff check.
-    event.relation.data[bucket].update({"data": json.dumps(new_data)})
-
-    # Return the diff with all possible changes.
-    return Diff(added, changed, deleted)
-
-
-# Base DataProvides and DataRequires
-
-
-class DataProvides(Object, ABC):
-    """Base provides-side of the data products relation."""
-
-    def __init__(self, charm: CharmBase, relation_name: str) -> None:
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.framework.observe(
-            charm.on[relation_name].relation_changed,
-            self._on_relation_changed,
-        )
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        return diff(event, self.local_app)
-
-    @abstractmethod
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the relation data has changed."""
-        raise NotImplementedError
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation id).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = {
-                key: value for key, value in relation.data[relation.app].items() if key != "data"
-            }
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return list(self.charm.model.relations[self.relation_name])
-
-    def set_credentials(self, relation_id: int, username: str, password: str) -> None:
-        """Set credentials.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            username: user that was created.
-            password: password of the created user.
-        """
-        self._update_relation_data(
-            relation_id,
-            {
-                "username": username,
-                "password": password,
-            },
-        )
-
-    def set_tls(self, relation_id: int, tls: str) -> None:
-        """Set whether TLS is enabled.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            tls: whether tls is enabled (True or False).
-        """
-        self._update_relation_data(relation_id, {"tls": tls})
-
-    def set_tls_ca(self, relation_id: int, tls_ca: str) -> None:
-        """Set the TLS CA in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            tls_ca: TLS certification authority.
-        """
-        self._update_relation_data(relation_id, {"tls_ca": tls_ca})
-
-
-class DataRequires(Object, ABC):
-    """Requires-side of the relation."""
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        extra_user_roles: str = None,
-    ):
-        """Manager of base client relations."""
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.extra_user_roles = extra_user_roles
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined, self._on_relation_joined_event
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed, self._on_relation_changed_event
-        )
-
-    @abstractmethod
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the relation."""
-        raise NotImplementedError
-
-    @abstractmethod
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        raise NotImplementedError
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-        Function cannot be used in `*-relation-broken` events and will raise an exception.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation ID).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = {
-                key: value for key, value in relation.data[relation.app].items() if key != "data"
-            }
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        return diff(event, self.local_unit)
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return [
-            relation
-            for relation in self.charm.model.relations[self.relation_name]
-            if self._is_relation_active(relation)
-        ]
-
-    @staticmethod
-    def _is_relation_active(relation: Relation):
-        try:
-            _ = repr(relation.data)
-            return True
-        except RuntimeError:
-            return False
-
-    @staticmethod
-    def _is_resource_created_for_relation(relation: Relation):
-        return (
-            "username" in relation.data[relation.app] and "password" in relation.data[relation.app]
-        )
-
-    def is_resource_created(self, relation_id: Optional[int] = None) -> bool:
-        """Check if the resource has been created.
-
-        This function can be used to check if the Provider answered with data in the charm code
-        when outside an event callback.
-
-        Args:
-            relation_id (int, optional): When provided the check is done only for the relation id
-                provided, otherwise the check is done for all relations
-
-        Returns:
-            True or False
-
-        Raises:
-            IndexError: If relation_id is provided but that relation does not exist
-        """
-        if relation_id is not None:
-            try:
-                relation = [relation for relation in self.relations if relation.id == relation_id][
-                    0
-                ]
-                return self._is_resource_created_for_relation(relation)
-            except IndexError:
-                raise IndexError(f"relation id {relation_id} cannot be accessed")
-        else:
-            return (
-                all(
-                    [
-                        self._is_resource_created_for_relation(relation)
-                        for relation in self.relations
-                    ]
-                )
-                if self.relations
-                else False
-            )
-
-
-# General events
-
-
-class ExtraRoleEvent(RelationEvent):
-    """Base class for data events."""
-
-    @property
-    def extra_user_roles(self) -> Optional[str]:
-        """Returns the extra user roles that were requested."""
-        return self.relation.data[self.relation.app].get("extra-user-roles")
-
-
-class AuthenticationEvent(RelationEvent):
-    """Base class for authentication fields for events."""
-
-    @property
-    def username(self) -> Optional[str]:
-        """Returns the created username."""
-        return self.relation.data[self.relation.app].get("username")
-
-    @property
-    def password(self) -> Optional[str]:
-        """Returns the password for the created user."""
-        return self.relation.data[self.relation.app].get("password")
-
-    @property
-    def tls(self) -> Optional[str]:
-        """Returns whether TLS is configured."""
-        return self.relation.data[self.relation.app].get("tls")
-
-    @property
-    def tls_ca(self) -> Optional[str]:
-        """Returns TLS CA."""
-        return self.relation.data[self.relation.app].get("tls-ca")
-
-
-# Database related events and fields
-
-
-class DatabaseProvidesEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def database(self) -> Optional[str]:
-        """Returns the database that was requested."""
-        return self.relation.data[self.relation.app].get("database")
-
-
-class DatabaseRequestedEvent(DatabaseProvidesEvent, ExtraRoleEvent):
-    """Event emitted when a new database is requested for use on this relation."""
-
-
-class DatabaseProvidesEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_requested = EventSource(DatabaseRequestedEvent)
-
-
-class DatabaseRequiresEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read/write endpoints."""
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def read_only_endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read only endpoints."""
-        return self.relation.data[self.relation.app].get("read-only-endpoints")
-
-    @property
-    def replset(self) -> Optional[str]:
-        """Returns the replicaset name.
-
-        MongoDB only.
-        """
-        return self.relation.data[self.relation.app].get("replset")
-
-    @property
-    def uris(self) -> Optional[str]:
-        """Returns the connection URIs.
-
-        MongoDB, Redis, OpenSearch.
-        """
-        return self.relation.data[self.relation.app].get("uris")
-
-    @property
-    def version(self) -> Optional[str]:
-        """Returns the version of the database.
-
-        Version as informed by the database daemon.
-        """
-        return self.relation.data[self.relation.app].get("version")
-
-
-class DatabaseCreatedEvent(AuthenticationEvent, DatabaseRequiresEvent):
-    """Event emitted when a new database is created for use on this relation."""
-
-
-class DatabaseEndpointsChangedEvent(AuthenticationEvent, DatabaseRequiresEvent):
-    """Event emitted when the read/write endpoints are changed."""
-
-
-class DatabaseReadOnlyEndpointsChangedEvent(AuthenticationEvent, DatabaseRequiresEvent):
-    """Event emitted when the read only endpoints are changed."""
-
-
-class DatabaseRequiresEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_created = EventSource(DatabaseCreatedEvent)
-    endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
-    read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent)
-
-
-# Database Provider and Requires
-
-
-class DatabaseProvides(DataProvides):
-    """Provider-side of the database relations."""
-
-    on = DatabaseProvidesEvents()
-
-    def __init__(self, charm: CharmBase, relation_name: str) -> None:
-        super().__init__(charm, relation_name)
-
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the relation has changed."""
-        # Only the leader should handle this event.
-        if not self.local_unit.is_leader():
-            return
-
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Emit a database requested event if the setup key (database name and optional
-        # extra user roles) was added to the relation databag by the application.
-        if "database" in diff.added:
-            self.on.database_requested.emit(event.relation, app=event.app, unit=event.unit)
-
-    def set_endpoints(self, relation_id: int, connection_strings: str) -> None:
-        """Set database primary connections.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            connection_strings: database hosts and ports comma separated list.
-        """
-        self._update_relation_data(relation_id, {"endpoints": connection_strings})
-
-    def set_read_only_endpoints(self, relation_id: int, connection_strings: str) -> None:
-        """Set database replicas connection strings.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            connection_strings: database hosts and ports comma separated list.
-        """
-        self._update_relation_data(relation_id, {"read-only-endpoints": connection_strings})
-
-    def set_replset(self, relation_id: int, replset: str) -> None:
-        """Set replica set name in the application relation databag.
-
-        MongoDB only.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            replset: replica set name.
-        """
-        self._update_relation_data(relation_id, {"replset": replset})
-
-    def set_uris(self, relation_id: int, uris: str) -> None:
-        """Set the database connection URIs in the application relation databag.
-
-        MongoDB, Redis, and OpenSearch only.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            uris: connection URIs.
-        """
-        self._update_relation_data(relation_id, {"uris": uris})
-
-    def set_version(self, relation_id: int, version: str) -> None:
-        """Set the database version in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            version: database version.
-        """
-        self._update_relation_data(relation_id, {"version": version})
-
-
-class DatabaseRequires(DataRequires):
-    """Requires-side of the database relation."""
-
-    on = DatabaseRequiresEvents()
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        database_name: str,
-        extra_user_roles: str = None,
-        relations_aliases: List[str] = None,
-    ):
-        """Manager of database client relations."""
-        super().__init__(charm, relation_name, extra_user_roles)
-        self.database = database_name
-        self.relations_aliases = relations_aliases
-
-        # Define custom event names for each alias.
-        if relations_aliases:
-            # Ensure the number of aliases does not exceed the maximum
-            # of connections allowed in the specific relation.
-            relation_connection_limit = self.charm.meta.requires[relation_name].limit
-            if len(relations_aliases) != relation_connection_limit:
-                raise ValueError(
-                    f"The number of aliases must match the maximum number of connections allowed in the relation. "
-                    f"Expected {relation_connection_limit}, got {len(relations_aliases)}"
-                )
-
-            for relation_alias in relations_aliases:
-                self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent)
-                self.on.define_event(
-                    f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent
-                )
-                self.on.define_event(
-                    f"{relation_alias}_read_only_endpoints_changed",
-                    DatabaseReadOnlyEndpointsChangedEvent,
-                )
-
-    def _assign_relation_alias(self, relation_id: int) -> None:
-        """Assigns an alias to a relation.
-
-        This function writes in the unit data bag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-        """
-        # If no aliases were provided, return immediately.
-        if not self.relations_aliases:
-            return
-
-        # Return if an alias was already assigned to this relation
-        # (like when there are more than one unit joining the relation).
-        if (
-            self.charm.model.get_relation(self.relation_name, relation_id)
-            .data[self.local_unit]
-            .get("alias")
-        ):
-            return
-
-        # Retrieve the available aliases (the ones that weren't assigned to any relation).
-        available_aliases = self.relations_aliases[:]
-        for relation in self.charm.model.relations[self.relation_name]:
-            alias = relation.data[self.local_unit].get("alias")
-            if alias:
-                logger.debug("Alias %s was already assigned to relation %d", alias, relation.id)
-                available_aliases.remove(alias)
-
-        # Set the alias in the unit relation databag of the specific relation.
-        relation = self.charm.model.get_relation(self.relation_name, relation_id)
-        relation.data[self.local_unit].update({"alias": available_aliases[0]})
-
-    def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None:
-        """Emit an aliased event to a particular relation if it has an alias.
-
-        Args:
-            event: the relation changed event that was received.
-            event_name: the name of the event to emit.
-        """
-        alias = self._get_relation_alias(event.relation.id)
-        if alias:
-            getattr(self.on, f"{alias}_{event_name}").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-    def _get_relation_alias(self, relation_id: int) -> Optional[str]:
-        """Returns the relation alias.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-
-        Returns:
-            the relation alias or None if the relation was not found.
-        """
-        for relation in self.charm.model.relations[self.relation_name]:
-            if relation.id == relation_id:
-                return relation.data[self.local_unit].get("alias")
-        return None
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the database relation."""
-        # If relations aliases were provided, assign one to the relation.
-        self._assign_relation_alias(event.relation.id)
-
-        # Sets both database and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the database.
-        if self.extra_user_roles:
-            self._update_relation_data(
-                event.relation.id,
-                {
-                    "database": self.database,
-                    "extra-user-roles": self.extra_user_roles,
-                },
-            )
-        else:
-            self._update_relation_data(event.relation.id, {"database": self.database})
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the database relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the database is created
-        # (the database charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("database created at %s", datetime.now())
-            self.on.database_created.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "database_created")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “database_created“ is triggered.
-            return
-
-        # Emit an endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "endpoints_changed")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered.
-            return
-
-        # Emit a read only endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("read-only-endpoints changed on %s", datetime.now())
-            self.on.read_only_endpoints_changed.emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "read_only_endpoints_changed")
-
-
-# Kafka related events
-
-
-class KafkaProvidesEvent(RelationEvent):
-    """Base class for Kafka events."""
-
-    @property
-    def topic(self) -> Optional[str]:
-        """Returns the topic that was requested."""
-        return self.relation.data[self.relation.app].get("topic")
-
-
-class TopicRequestedEvent(KafkaProvidesEvent, ExtraRoleEvent):
-    """Event emitted when a new topic is requested for use on this relation."""
-
-
-class KafkaProvidesEvents(CharmEvents):
-    """Kafka events.
-
-    This class defines the events that the Kafka can emit.
-    """
-
-    topic_requested = EventSource(TopicRequestedEvent)
-
-
-class KafkaRequiresEvent(RelationEvent):
-    """Base class for Kafka events."""
-
-    @property
-    def bootstrap_server(self) -> Optional[str]:
-        """Returns a a comma-seperated list of broker uris."""
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def consumer_group_prefix(self) -> Optional[str]:
-        """Returns the consumer-group-prefix."""
-        return self.relation.data[self.relation.app].get("consumer-group-prefix")
-
-    @property
-    def zookeeper_uris(self) -> Optional[str]:
-        """Returns a comma separated list of Zookeeper uris."""
-        return self.relation.data[self.relation.app].get("zookeeper-uris")
-
-
-class TopicCreatedEvent(AuthenticationEvent, KafkaRequiresEvent):
-    """Event emitted when a new topic is created for use on this relation."""
-
-
-class BootstrapServerChangedEvent(AuthenticationEvent, KafkaRequiresEvent):
-    """Event emitted when the bootstrap server is changed."""
-
-
-class KafkaRequiresEvents(CharmEvents):
-    """Kafka events.
-
-    This class defines the events that the Kafka can emit.
-    """
-
-    topic_created = EventSource(TopicCreatedEvent)
-    bootstrap_server_changed = EventSource(BootstrapServerChangedEvent)
-
-
-# Kafka Provides and Requires
-
-
-class KafkaProvides(DataProvides):
-    """Provider-side of the Kafka relation."""
-
-    on = KafkaProvidesEvents()
-
-    def __init__(self, charm: CharmBase, relation_name: str) -> None:
-        super().__init__(charm, relation_name)
-
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the relation has changed."""
-        # Only the leader should handle this event.
-        if not self.local_unit.is_leader():
-            return
-
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Emit a topic requested event if the setup key (topic name and optional
-        # extra user roles) was added to the relation databag by the application.
-        if "topic" in diff.added:
-            self.on.topic_requested.emit(event.relation, app=event.app, unit=event.unit)
-
-    def set_bootstrap_server(self, relation_id: int, bootstrap_server: str) -> None:
-        """Set the bootstrap server in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            bootstrap_server: the bootstrap server address.
-        """
-        self._update_relation_data(relation_id, {"endpoints": bootstrap_server})
-
-    def set_consumer_group_prefix(self, relation_id: int, consumer_group_prefix: str) -> None:
-        """Set the consumer group prefix in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            consumer_group_prefix: the consumer group prefix string.
-        """
-        self._update_relation_data(relation_id, {"consumer-group-prefix": consumer_group_prefix})
-
-    def set_zookeeper_uris(self, relation_id: int, zookeeper_uris: str) -> None:
-        """Set the zookeeper uris in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            zookeeper_uris: comma-seperated list of ZooKeeper server uris.
-        """
-        self._update_relation_data(relation_id, {"zookeeper-uris": zookeeper_uris})
-
-
-class KafkaRequires(DataRequires):
-    """Requires-side of the Kafka relation."""
-
-    on = KafkaRequiresEvents()
-
-    def __init__(self, charm, relation_name: str, topic: str, extra_user_roles: str = None):
-        """Manager of Kafka client relations."""
-        # super().__init__(charm, relation_name)
-        super().__init__(charm, relation_name, extra_user_roles)
-        self.charm = charm
-        self.topic = topic
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the Kafka relation."""
-        # Sets both topic and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the topic.
-        self._update_relation_data(
-            event.relation.id,
-            {
-                "topic": self.topic,
-                "extra-user-roles": self.extra_user_roles,
-            }
-            if self.extra_user_roles is not None
-            else {"topic": self.topic},
-        )
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the Kafka relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the topic is created
-        # (the Kafka charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("topic created at %s", datetime.now())
-            self.on.topic_created.emit(event.relation, app=event.app, unit=event.unit)
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “topic_created“ is triggered.
-            return
-
-        # Emit an endpoints (bootstap-server) changed event if the Kakfa endpoints
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            self.on.bootstrap_server_changed.emit(
-                event.relation, app=event.app, unit=event.unit
-            )  # here check if this is the right design
-            return
diff --git a/installers/charm/osm-pol/lib/charms/kafka_k8s/v0/kafka.py b/installers/charm/osm-pol/lib/charms/kafka_k8s/v0/kafka.py
deleted file mode 100644 (file)
index aeb5edc..0000000
+++ /dev/null
@@ -1,200 +0,0 @@
-# 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.
-
-"""Kafka library.
-
-This [library](https://juju.is/docs/sdk/libraries) implements both sides of the
-`kafka` [interface](https://juju.is/docs/sdk/relations).
-
-The *provider* side of this interface is implemented by the
-[kafka-k8s Charmed Operator](https://charmhub.io/kafka-k8s).
-
-Any Charmed Operator that *requires* Kafka for providing its
-service should implement the *requirer* side of this interface.
-
-In a nutshell using this library to implement a Charmed Operator *requiring*
-Kafka would look like
-
-```
-$ charmcraft fetch-lib charms.kafka_k8s.v0.kafka
-```
-
-`metadata.yaml`:
-
-```
-requires:
-  kafka:
-    interface: kafka
-    limit: 1
-```
-
-`src/charm.py`:
-
-```
-from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
-from ops.charm import CharmBase
-
-
-class MyCharm(CharmBase):
-
-    on = KafkaEvents()
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.kafka = KafkaRequires(self)
-        self.framework.observe(
-            self.on.kafka_available,
-            self._on_kafka_available,
-        )
-        self.framework.observe(
-            self.on["kafka"].relation_broken,
-            self._on_kafka_broken,
-        )
-
-    def _on_kafka_available(self, event):
-        # Get Kafka host and port
-        host: str = self.kafka.host
-        port: int = self.kafka.port
-        # host => "kafka-k8s"
-        # port => 9092
-
-    def _on_kafka_broken(self, event):
-        # Stop service
-        # ...
-        self.unit.status = BlockedStatus("need kafka relation")
-```
-
-You can file bugs
-[here](https://github.com/charmed-osm/kafka-k8s-operator/issues)!
-"""
-
-from typing import Optional
-
-from ops.charm import CharmBase, CharmEvents
-from ops.framework import EventBase, EventSource, Object
-
-# The unique Charmhub library identifier, never change it
-from ops.model import Relation
-
-LIBID = "eacc8c85082347c9aae740e0220b8376"
-
-# 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 = 4
-
-
-KAFKA_HOST_APP_KEY = "host"
-KAFKA_PORT_APP_KEY = "port"
-
-
-class _KafkaAvailableEvent(EventBase):
-    """Event emitted when Kafka is available."""
-
-
-class KafkaEvents(CharmEvents):
-    """Kafka events.
-
-    This class defines the events that Kafka can emit.
-
-    Events:
-        kafka_available (_KafkaAvailableEvent)
-    """
-
-    kafka_available = EventSource(_KafkaAvailableEvent)
-
-
-class KafkaRequires(Object):
-    """Requires-side of the Kafka relation."""
-
-    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
-        super().__init__(charm, endpoint_name)
-        self.charm = charm
-        self._endpoint_name = endpoint_name
-
-        # Observe relation events
-        event_observe_mapping = {
-            charm.on[self._endpoint_name].relation_changed: self._on_relation_changed,
-        }
-        for event, observer in event_observe_mapping.items():
-            self.framework.observe(event, observer)
-
-    def _on_relation_changed(self, event) -> None:
-        if event.relation.app and all(
-            key in event.relation.data[event.relation.app]
-            for key in (KAFKA_HOST_APP_KEY, KAFKA_PORT_APP_KEY)
-        ):
-            self.charm.on.kafka_available.emit()
-
-    @property
-    def host(self) -> str:
-        """Get kafka hostname."""
-        relation: Relation = self.model.get_relation(self._endpoint_name)
-        return (
-            relation.data[relation.app].get(KAFKA_HOST_APP_KEY)
-            if relation and relation.app
-            else None
-        )
-
-    @property
-    def port(self) -> int:
-        """Get kafka port number."""
-        relation: Relation = self.model.get_relation(self._endpoint_name)
-        return (
-            int(relation.data[relation.app].get(KAFKA_PORT_APP_KEY))
-            if relation and relation.app
-            else None
-        )
-
-
-class KafkaProvides(Object):
-    """Provides-side of the Kafka relation."""
-
-    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
-        super().__init__(charm, endpoint_name)
-        self._endpoint_name = endpoint_name
-
-    def set_host_info(self, host: str, port: int, relation: Optional[Relation] = None) -> None:
-        """Set Kafka host and port.
-
-        This function writes in the application data of the relation, therefore,
-        only the unit leader can call it.
-
-        Args:
-            host (str): Kafka hostname or IP address.
-            port (int): Kafka port.
-            relation (Optional[Relation]): Relation to update.
-                                           If not specified, all relations will be updated.
-
-        Raises:
-            Exception: if a non-leader unit calls this function.
-        """
-        if not self.model.unit.is_leader():
-            raise Exception("only the leader set host information.")
-
-        if relation:
-            self._update_relation_data(host, port, relation)
-            return
-
-        for relation in self.model.relations[self._endpoint_name]:
-            self._update_relation_data(host, port, relation)
-
-    def _update_relation_data(self, host: str, port: int, relation: Relation) -> None:
-        """Update data in relation if needed."""
-        relation.data[self.model.app][KAFKA_HOST_APP_KEY] = host
-        relation.data[self.model.app][KAFKA_PORT_APP_KEY] = str(port)
diff --git a/installers/charm/osm-pol/lib/charms/osm_libs/v0/utils.py b/installers/charm/osm-pol/lib/charms/osm_libs/v0/utils.py
deleted file mode 100644 (file)
index d739ba6..0000000
+++ /dev/null
@@ -1,544 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-#         http://www.apache.org/licenses/LICENSE-2.0
-"""OSM Utils Library.
-
-This library offers some utilities made for but not limited to Charmed OSM.
-
-# Getting started
-
-Execute the following command inside your Charmed Operator folder to fetch the library.
-
-```shell
-charmcraft fetch-lib charms.osm_libs.v0.utils
-```
-
-# CharmError Exception
-
-An exception that takes to arguments, the message and the StatusBase class, which are useful
-to set the status of the charm when the exception raises.
-
-Example:
-```shell
-from charms.osm_libs.v0.utils import CharmError
-
-class MyCharm(CharmBase):
-    def _on_config_changed(self, _):
-        try:
-            if not self.config.get("some-option"):
-                raise CharmError("need some-option", BlockedStatus)
-
-            if not self.mysql_ready:
-                raise CharmError("waiting for mysql", WaitingStatus)
-
-            # Do stuff...
-
-        exception CharmError as e:
-            self.unit.status = e.status
-```
-
-# Pebble validations
-
-The `check_container_ready` function checks that a container is ready,
-and therefore Pebble is ready.
-
-The `check_service_active` function checks that a service in a container is running.
-
-Both functions raise a CharmError if the validations fail.
-
-Example:
-```shell
-from charms.osm_libs.v0.utils import check_container_ready, check_service_active
-
-class MyCharm(CharmBase):
-    def _on_config_changed(self, _):
-        try:
-            container: Container = self.unit.get_container("my-container")
-            check_container_ready(container)
-            check_service_active(container, "my-service")
-            # Do stuff...
-
-        exception CharmError as e:
-            self.unit.status = e.status
-```
-
-# Debug-mode
-
-The debug-mode allows OSM developers to easily debug OSM modules.
-
-Example:
-```shell
-from charms.osm_libs.v0.utils import DebugMode
-
-class MyCharm(CharmBase):
-    _stored = StoredState()
-
-    def __init__(self, _):
-        # ...
-        container: Container = self.unit.get_container("my-container")
-        hostpaths = [
-            HostPath(
-                config="module-hostpath",
-                container_path="/usr/lib/python3/dist-packages/module"
-            ),
-        ]
-        vscode_workspace_path = "files/vscode-workspace.json"
-        self.debug_mode = DebugMode(
-            self,
-            self._stored,
-            container,
-            hostpaths,
-            vscode_workspace_path,
-        )
-
-    def _on_update_status(self, _):
-        if self.debug_mode.started:
-            return
-        # ...
-
-    def _get_debug_mode_information(self):
-        command = self.debug_mode.command
-        password = self.debug_mode.password
-        return command, password
-```
-
-# More
-
-- Get pod IP with `get_pod_ip()`
-"""
-from dataclasses import dataclass
-import logging
-import secrets
-import socket
-from pathlib import Path
-from typing import List
-
-from lightkube import Client
-from lightkube.models.core_v1 import HostPathVolumeSource, Volume, VolumeMount
-from lightkube.resources.apps_v1 import StatefulSet
-from ops.charm import CharmBase
-from ops.framework import Object, StoredState
-from ops.model import (
-    ActiveStatus,
-    BlockedStatus,
-    Container,
-    MaintenanceStatus,
-    StatusBase,
-    WaitingStatus,
-)
-from ops.pebble import ServiceStatus
-
-# The unique Charmhub library identifier, never change it
-LIBID = "e915908eebee4cdd972d484728adf984"
-
-# 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 = 5
-
-logger = logging.getLogger(__name__)
-
-
-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)
-
-
-def check_container_ready(container: Container) -> None:
-    """Check Pebble has started in the container.
-
-    Args:
-        container (Container): Container to be checked.
-
-    Raises:
-        CharmError: if container is not ready.
-    """
-    if not container.can_connect():
-        raise CharmError("waiting for pebble to start", MaintenanceStatus)
-
-
-def check_service_active(container: Container, service_name: str) -> None:
-    """Check if the service is running.
-
-    Args:
-        container (Container): Container to be checked.
-        service_name (str): Name of the service to check.
-
-    Raises:
-        CharmError: if the service is not running.
-    """
-    if service_name not in container.get_plan().services:
-        raise CharmError(f"{service_name} service not configured yet", WaitingStatus)
-
-    if container.get_service(service_name).current != ServiceStatus.ACTIVE:
-        raise CharmError(f"{service_name} service is not running")
-
-
-def get_pod_ip() -> str:
-    """Get Kubernetes Pod IP.
-
-    Returns:
-        str: The IP of the Pod.
-    """
-    return socket.gethostbyname(socket.gethostname())
-
-
-_DEBUG_SCRIPT = r"""#!/bin/bash
-# Install SSH
-
-function download_code(){{
-    wget https://go.microsoft.com/fwlink/?LinkID=760868 -O code.deb
-}}
-
-function setup_envs(){{
-    grep "source /debug.envs" /root/.bashrc || echo "source /debug.envs" | tee -a /root/.bashrc
-}}
-function setup_ssh(){{
-    apt install ssh -y
-    cat /etc/ssh/sshd_config |
-        grep -E '^PermitRootLogin yes$$' || (
-        echo PermitRootLogin yes |
-        tee -a /etc/ssh/sshd_config
-    )
-    service ssh stop
-    sleep 3
-    service ssh start
-    usermod --password $(echo {} | openssl passwd -1 -stdin) root
-}}
-
-function setup_code(){{
-    apt install libasound2 -y
-    (dpkg -i code.deb || apt-get install -f -y || apt-get install -f -y) && echo Code installed successfully
-    code --install-extension ms-python.python --user-data-dir /root
-    mkdir -p /root/.vscode-server
-    cp -R /root/.vscode/extensions /root/.vscode-server/extensions
-}}
-
-export DEBIAN_FRONTEND=noninteractive
-apt update && apt install wget -y
-download_code &
-setup_ssh &
-setup_envs
-wait
-setup_code &
-wait
-"""
-
-
-@dataclass
-class SubModule:
-    """Represent RO Submodules."""
-    sub_module_path: str
-    container_path: str
-
-
-class HostPath:
-    """Represents a hostpath."""
-    def __init__(self, config: str, container_path: str, submodules: dict = None) -> None:
-        mount_path_items = config.split("-")
-        mount_path_items.reverse()
-        self.mount_path = "/" + "/".join(mount_path_items)
-        self.config = config
-        self.sub_module_dict = {}
-        if submodules:
-            for submodule in submodules.keys():
-                self.sub_module_dict[submodule] = SubModule(
-                    sub_module_path=self.mount_path + "/" + submodule + "/" + submodules[submodule].split("/")[-1],
-                    container_path=submodules[submodule],
-                )
-        else:
-            self.container_path = container_path
-            self.module_name = container_path.split("/")[-1]
-
-class DebugMode(Object):
-    """Class to handle the debug-mode."""
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        stored: StoredState,
-        container: Container,
-        hostpaths: List[HostPath] = [],
-        vscode_workspace_path: str = "files/vscode-workspace.json",
-    ) -> None:
-        super().__init__(charm, "debug-mode")
-
-        self.charm = charm
-        self._stored = stored
-        self.hostpaths = hostpaths
-        self.vscode_workspace = Path(vscode_workspace_path).read_text()
-        self.container = container
-
-        self._stored.set_default(
-            debug_mode_started=False,
-            debug_mode_vscode_command=None,
-            debug_mode_password=None,
-        )
-
-        self.framework.observe(self.charm.on.config_changed, self._on_config_changed)
-        self.framework.observe(self.charm.on[container.name].pebble_ready, self._on_config_changed)
-        self.framework.observe(self.charm.on.update_status, self._on_update_status)
-
-    def _on_config_changed(self, _) -> None:
-        """Handler for the config-changed event."""
-        if not self.charm.unit.is_leader():
-            return
-
-        debug_mode_enabled = self.charm.config.get("debug-mode", False)
-        action = self.enable if debug_mode_enabled else self.disable
-        action()
-
-    def _on_update_status(self, _) -> None:
-        """Handler for the update-status event."""
-        if not self.charm.unit.is_leader() or not self.started:
-            return
-
-        self.charm.unit.status = ActiveStatus("debug-mode: ready")
-
-    @property
-    def started(self) -> bool:
-        """Indicates whether the debug-mode has started or not."""
-        return self._stored.debug_mode_started
-
-    @property
-    def command(self) -> str:
-        """Command to launch vscode."""
-        return self._stored.debug_mode_vscode_command
-
-    @property
-    def password(self) -> str:
-        """SSH password."""
-        return self._stored.debug_mode_password
-
-    def enable(self, service_name: str = None) -> None:
-        """Enable debug-mode.
-
-        This function mounts hostpaths of the OSM modules (if set), and
-        configures the container so it can be easily debugged. The setup
-        includes the configuration of SSH, environment variables, and
-        VSCode workspace and plugins.
-
-        Args:
-            service_name (str, optional): Pebble service name which has the desired environment
-                variables. Mandatory if there is more than one Pebble service configured.
-        """
-        hostpaths_to_reconfigure = self._hostpaths_to_reconfigure()
-        if self.started and not hostpaths_to_reconfigure:
-            self.charm.unit.status = ActiveStatus("debug-mode: ready")
-            return
-
-        logger.debug("enabling debug-mode")
-
-        # Mount hostpaths if set.
-        # If hostpaths are mounted, the statefulset will be restarted,
-        # and for that reason we return immediately. On restart, the hostpaths
-        # won't be mounted and then we can continue and setup the debug-mode.
-        if hostpaths_to_reconfigure:
-            self.charm.unit.status = MaintenanceStatus("debug-mode: configuring hostpaths")
-            self._configure_hostpaths(hostpaths_to_reconfigure)
-            return
-
-        self.charm.unit.status = MaintenanceStatus("debug-mode: starting")
-        password = secrets.token_hex(8)
-        self._setup_debug_mode(
-            password,
-            service_name,
-            mounted_hostpaths=[hp for hp in self.hostpaths if self.charm.config.get(hp.config)],
-        )
-
-        self._stored.debug_mode_vscode_command = self._get_vscode_command(get_pod_ip())
-        self._stored.debug_mode_password = password
-        self._stored.debug_mode_started = True
-        logger.info("debug-mode is ready")
-        self.charm.unit.status = ActiveStatus("debug-mode: ready")
-
-    def disable(self) -> None:
-        """Disable debug-mode."""
-        logger.debug("disabling debug-mode")
-        current_status = self.charm.unit.status
-        hostpaths_unmounted = self._unmount_hostpaths()
-
-        if not self._stored.debug_mode_started:
-            return
-        self._stored.debug_mode_started = False
-        self._stored.debug_mode_vscode_command = None
-        self._stored.debug_mode_password = None
-
-        if not hostpaths_unmounted:
-            self.charm.unit.status = current_status
-            self._restart()
-
-    def _hostpaths_to_reconfigure(self) -> List[HostPath]:
-        hostpaths_to_reconfigure: List[HostPath] = []
-        client = Client()
-        statefulset = client.get(StatefulSet, self.charm.app.name, namespace=self.charm.model.name)
-        volumes = statefulset.spec.template.spec.volumes
-
-        for hostpath in self.hostpaths:
-            hostpath_is_set = True if self.charm.config.get(hostpath.config) else False
-            hostpath_already_configured = next(
-                (True for volume in volumes if volume.name == hostpath.config), False
-            )
-            if hostpath_is_set != hostpath_already_configured:
-                hostpaths_to_reconfigure.append(hostpath)
-
-        return hostpaths_to_reconfigure
-
-    def _setup_debug_mode(
-        self,
-        password: str,
-        service_name: str = None,
-        mounted_hostpaths: List[HostPath] = [],
-    ) -> None:
-        services = self.container.get_plan().services
-        if not service_name and len(services) != 1:
-            raise Exception("Cannot start debug-mode: please set the service_name")
-
-        service = None
-        if not service_name:
-            service_name, service = services.popitem()
-        if not service:
-            service = services.get(service_name)
-
-        logger.debug(f"getting environment variables from service {service_name}")
-        environment = service.environment
-        environment_file_content = "\n".join(
-            [f'export {key}="{value}"' for key, value in environment.items()]
-        )
-        logger.debug(f"pushing environment file to {self.container.name} container")
-        self.container.push("/debug.envs", environment_file_content)
-
-        # Push VSCode workspace
-        logger.debug(f"pushing vscode workspace to {self.container.name} container")
-        self.container.push("/debug.code-workspace", self.vscode_workspace)
-
-        # Execute debugging script
-        logger.debug(f"pushing debug-mode setup script to {self.container.name} container")
-        self.container.push("/debug.sh", _DEBUG_SCRIPT.format(password), permissions=0o777)
-        logger.debug(f"executing debug-mode setup script in {self.container.name} container")
-        self.container.exec(["/debug.sh"]).wait_output()
-        logger.debug(f"stopping service {service_name} in {self.container.name} container")
-        self.container.stop(service_name)
-
-        # Add symlinks to mounted hostpaths
-        for hostpath in mounted_hostpaths:
-            logger.debug(f"adding symlink for {hostpath.config}")
-            if len(hostpath.sub_module_dict) > 0:
-                for sub_module in hostpath.sub_module_dict.keys():
-                    self.container.exec(["rm", "-rf", hostpath.sub_module_dict[sub_module].container_path]).wait_output()
-                    self.container.exec(
-                        [
-                            "ln",
-                            "-s",
-                            hostpath.sub_module_dict[sub_module].sub_module_path,
-                            hostpath.sub_module_dict[sub_module].container_path,
-                        ]
-                    )
-
-            else:
-                self.container.exec(["rm", "-rf", hostpath.container_path]).wait_output()
-                self.container.exec(
-                    [
-                        "ln",
-                        "-s",
-                        f"{hostpath.mount_path}/{hostpath.module_name}",
-                        hostpath.container_path,
-                    ]
-                )
-
-    def _configure_hostpaths(self, hostpaths: List[HostPath]):
-        client = Client()
-        statefulset = client.get(StatefulSet, self.charm.app.name, namespace=self.charm.model.name)
-
-        for hostpath in hostpaths:
-            if self.charm.config.get(hostpath.config):
-                self._add_hostpath_to_statefulset(hostpath, statefulset)
-            else:
-                self._delete_hostpath_from_statefulset(hostpath, statefulset)
-
-        client.replace(statefulset)
-
-    def _unmount_hostpaths(self) -> bool:
-        client = Client()
-        hostpath_unmounted = False
-        statefulset = client.get(StatefulSet, self.charm.app.name, namespace=self.charm.model.name)
-
-        for hostpath in self.hostpaths:
-            if self._delete_hostpath_from_statefulset(hostpath, statefulset):
-                hostpath_unmounted = True
-
-        if hostpath_unmounted:
-            client.replace(statefulset)
-
-        return hostpath_unmounted
-
-    def _add_hostpath_to_statefulset(self, hostpath: HostPath, statefulset: StatefulSet):
-        # Add volume
-        logger.debug(f"adding volume {hostpath.config} to {self.charm.app.name} statefulset")
-        volume = Volume(
-            hostpath.config,
-            hostPath=HostPathVolumeSource(
-                path=self.charm.config[hostpath.config],
-                type="Directory",
-            ),
-        )
-        statefulset.spec.template.spec.volumes.append(volume)
-
-        # Add volumeMount
-        for statefulset_container in statefulset.spec.template.spec.containers:
-            if statefulset_container.name != self.container.name:
-                continue
-
-            logger.debug(
-                f"adding volumeMount {hostpath.config} to {self.container.name} container"
-            )
-            statefulset_container.volumeMounts.append(
-                VolumeMount(mountPath=hostpath.mount_path, name=hostpath.config)
-            )
-
-    def _delete_hostpath_from_statefulset(self, hostpath: HostPath, statefulset: StatefulSet):
-        hostpath_unmounted = False
-        for volume in statefulset.spec.template.spec.volumes:
-
-            if hostpath.config != volume.name:
-                continue
-
-            # Remove volumeMount
-            for statefulset_container in statefulset.spec.template.spec.containers:
-                if statefulset_container.name != self.container.name:
-                    continue
-                for volume_mount in statefulset_container.volumeMounts:
-                    if volume_mount.name != hostpath.config:
-                        continue
-
-                    logger.debug(
-                        f"removing volumeMount {hostpath.config} from {self.container.name} container"
-                    )
-                    statefulset_container.volumeMounts.remove(volume_mount)
-
-            # Remove volume
-            logger.debug(
-                f"removing volume {hostpath.config} from {self.charm.app.name} statefulset"
-            )
-            statefulset.spec.template.spec.volumes.remove(volume)
-
-            hostpath_unmounted = True
-        return hostpath_unmounted
-
-    def _get_vscode_command(
-        self,
-        pod_ip: str,
-        user: str = "root",
-        workspace_path: str = "/debug.code-workspace",
-    ) -> str:
-        return f"code --remote ssh-remote+{user}@{pod_ip} {workspace_path}"
-
-    def _restart(self):
-        self.container.exec(["kill", "-HUP", "1"])
diff --git a/installers/charm/osm-pol/metadata.yaml b/installers/charm/osm-pol/metadata.yaml
deleted file mode 100644 (file)
index adf189a..0000000
+++ /dev/null
@@ -1,63 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Overview on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
-
-name: osm-pol
-
-# The following metadata are human-readable and will be published prominently on Charmhub.
-
-display-name: OSM POL
-
-summary: OSM Policy module (POL)
-
-description: |
-  A Kubernetes operator that deploys the Policy module of OSM.
-
-  TODO include description of the module!!!
-
-  This charm doesn't make sense on its own.
-  See more:
-    - https://charmhub.io/osm
-
-containers:
-  pol:
-    resource: pol-image
-
-# This file populates the Resources tab on Charmhub.
-
-resources:
-  pol-image:
-    type: oci-image
-    description: OCI image for pol
-    upstream-source: opensourcemano/pol
-
-requires:
-  kafka:
-    interface: kafka
-    limit: 1
-  mongodb:
-    interface: mongodb_client
-    limit: 1
-  mysql:
-    interface: mysql
-    limit: 1
diff --git a/installers/charm/osm-pol/pyproject.toml b/installers/charm/osm-pol/pyproject.toml
deleted file mode 100644 (file)
index 16cf0f4..0000000
+++ /dev/null
@@ -1,52 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-
-# 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", "E501", "D107"]
-# D100, D101, D102, D103: Ignore missing docstrings in tests
-per-file-ignores = ["tests/*:D100,D101,D102,D103,D104"]
-docstring-convention = "google"
diff --git a/installers/charm/osm-pol/requirements.txt b/installers/charm/osm-pol/requirements.txt
deleted file mode 100644 (file)
index 398d4ad..0000000
+++ /dev/null
@@ -1,23 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-ops < 2.2
-lightkube
-lightkube-models
-# git+https://github.com/charmed-osm/config-validator/
diff --git a/installers/charm/osm-pol/src/charm.py b/installers/charm/osm-pol/src/charm.py
deleted file mode 100755 (executable)
index 07bf87e..0000000
+++ /dev/null
@@ -1,241 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# Learn more at: https://juju.is/docs/sdk
-
-"""OSM POL charm.
-
-See more: https://charmhub.io/osm
-"""
-
-import logging
-from typing import Any, Dict
-
-from charms.data_platform_libs.v0.data_interfaces import DatabaseRequires
-from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
-from charms.osm_libs.v0.utils import (
-    CharmError,
-    DebugMode,
-    HostPath,
-    check_container_ready,
-    check_service_active,
-)
-from ops.charm import ActionEvent, CharmBase
-from ops.framework import StoredState
-from ops.main import main
-from ops.model import ActiveStatus, Container
-
-from legacy_interfaces import MysqlClient
-
-HOSTPATHS = [
-    HostPath(
-        config="pol-hostpath",
-        container_path="/usr/lib/python3/dist-packages/osm_policy_module",
-    ),
-    HostPath(
-        config="common-hostpath",
-        container_path="/usr/lib/python3/dist-packages/osm_common",
-    ),
-]
-
-logger = logging.getLogger(__name__)
-
-
-class OsmPolCharm(CharmBase):
-    """OSM POL Kubernetes sidecar charm."""
-
-    on = KafkaEvents()
-    _stored = StoredState()
-    container_name = "pol"
-    service_name = "pol"
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        self.kafka = KafkaRequires(self)
-        self.mongodb_client = DatabaseRequires(self, "mongodb", database_name="osm")
-        self.mysql_client = MysqlClient(self, "mysql")
-        self._observe_charm_events()
-        self.container: Container = self.unit.get_container(self.container_name)
-        self.debug_mode = DebugMode(self, self._stored, self.container, HOSTPATHS)
-
-    # ---------------------------------------------------------------------------
-    #   Handlers for Charm Events
-    # ---------------------------------------------------------------------------
-
-    def _on_config_changed(self, _) -> None:
-        """Handler for the config-changed event."""
-        try:
-            self._validate_config()
-            self._check_relations()
-            # Check if the container is ready.
-            # Eventually it will become ready after the first pebble-ready event.
-            check_container_ready(self.container)
-
-            if not self.debug_mode.started:
-                self._configure_service(self.container)
-            # Update charm status
-            self._on_update_status()
-        except CharmError as e:
-            logger.debug(e.message)
-            self.unit.status = e.status
-
-    def _on_update_status(self, _=None) -> None:
-        """Handler for the update-status event."""
-        try:
-            self._validate_config()
-            self._check_relations()
-            check_container_ready(self.container)
-            if self.debug_mode.started:
-                return
-            check_service_active(self.container, self.service_name)
-            self.unit.status = ActiveStatus()
-        except CharmError as e:
-            logger.debug(e.message)
-            self.unit.status = e.status
-
-    def _on_required_relation_broken(self, _) -> None:
-        """Handler for the kafka-broken event."""
-        # Check Pebble has started in the container
-        try:
-            check_container_ready(self.container)
-            check_service_active(self.container, self.service_name)
-            self.container.stop(self.container_name)
-        except CharmError:
-            pass
-        self._on_update_status()
-
-    def _on_get_debug_mode_information_action(self, event: ActionEvent) -> None:
-        """Handler for the get-debug-mode-information action event."""
-        if not self.debug_mode.started:
-            event.fail("debug-mode has not started. Hint: juju config pol debug-mode=true")
-            return
-
-        debug_info = {"command": self.debug_mode.command, "password": self.debug_mode.password}
-        event.set_results(debug_info)
-
-    # ---------------------------------------------------------------------------
-    #   Validation and configuration and more
-    # ---------------------------------------------------------------------------
-
-    def _observe_charm_events(self) -> None:
-        event_handler_mapping = {
-            # Core lifecycle events
-            self.on.pol_pebble_ready: self._on_config_changed,
-            self.on.config_changed: self._on_config_changed,
-            self.on.update_status: self._on_update_status,
-            # Relation events
-            self.on.kafka_available: self._on_config_changed,
-            self.on["kafka"].relation_broken: self._on_required_relation_broken,
-            self.on["mysql"].relation_changed: self._on_config_changed,
-            self.on["mysql"].relation_broken: self._on_config_changed,
-            self.mongodb_client.on.database_created: self._on_config_changed,
-            self.on["mongodb"].relation_broken: self._on_required_relation_broken,
-            # Action events
-            self.on.get_debug_mode_information_action: self._on_get_debug_mode_information_action,
-        }
-
-        for event, handler in event_handler_mapping.items():
-            self.framework.observe(event, handler)
-
-    def _is_database_available(self) -> bool:
-        try:
-            return self.mongodb_client.is_resource_created()
-        except KeyError:
-            return False
-
-    def _validate_config(self) -> None:
-        """Validate charm configuration.
-
-        Raises:
-            CharmError: if charm configuration is invalid.
-        """
-        logger.debug("validating charm config")
-
-    def _check_relations(self) -> None:
-        """Validate charm relations.
-
-        Raises:
-            CharmError: if charm configuration is invalid.
-        """
-        logger.debug("check for missing relations")
-        missing_relations = []
-
-        if not self.kafka.host or not self.kafka.port:
-            missing_relations.append("kafka")
-        if not self._is_database_available():
-            missing_relations.append("mongodb")
-        if not self.config.get("mysql-uri") and self.mysql_client.is_missing_data_in_unit():
-            missing_relations.append("mysql")
-
-        if missing_relations:
-            relations_str = ", ".join(missing_relations)
-            one_relation_missing = len(missing_relations) == 1
-            error_msg = f'need {relations_str} relation{"" if one_relation_missing else "s"}'
-            logger.warning(error_msg)
-            raise CharmError(error_msg)
-
-    def _configure_service(self, container: Container) -> None:
-        """Add Pebble layer with the pol service."""
-        logger.debug(f"configuring {self.app.name} service")
-        container.add_layer("pol", self._get_layer(), combine=True)
-        container.replan()
-
-    def _get_layer(self) -> Dict[str, Any]:
-        """Get layer for Pebble."""
-        return {
-            "summary": "pol layer",
-            "description": "pebble config layer for pol",
-            "services": {
-                self.service_name: {
-                    "override": "replace",
-                    "summary": "pol service",
-                    "command": "/bin/bash scripts/start.sh",
-                    "startup": "enabled",
-                    "user": "appuser",
-                    "group": "appuser",
-                    "environment": {
-                        # General configuration
-                        "OSMPOL_GLOBAL_LOGLEVEL": self.config["log-level"],
-                        # Kafka configuration
-                        "OSMPOL_MESSAGE_HOST": self.kafka.host,
-                        "OSMPOL_MESSAGE_PORT": self.kafka.port,
-                        "OSMPOL_MESSAGE_DRIVER": "kafka",
-                        # Database Mongodb configuration
-                        "OSMPOL_DATABASE_DRIVER": "mongo",
-                        "OSMPOL_DATABASE_URI": self._get_mongodb_uri(),
-                        # Database MySQL configuration
-                        "OSMPOL_SQL_DATABASE_URI": self._get_mysql_uri(),
-                    },
-                }
-            },
-        }
-
-    def _get_mysql_uri(self):
-        return self.config.get("mysql-uri") or self.mysql_client.get_root_uri("pol")
-
-    def _get_mongodb_uri(self):
-        return list(self.mongodb_client.fetch_relation_data().values())[0]["uris"]
-
-
-if __name__ == "__main__":  # pragma: no cover
-    main(OsmPolCharm)
diff --git a/installers/charm/osm-pol/src/legacy_interfaces.py b/installers/charm/osm-pol/src/legacy_interfaces.py
deleted file mode 100644 (file)
index 443cba8..0000000
+++ /dev/null
@@ -1,165 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-# flake8: noqa
-
-import ops
-
-
-class BaseRelationClient(ops.framework.Object):
-    """Requires side of a Kafka Endpoint"""
-
-    def __init__(
-        self,
-        charm: ops.charm.CharmBase,
-        relation_name: str,
-        mandatory_fields: list = [],
-    ):
-        super().__init__(charm, relation_name)
-        self.relation_name = relation_name
-        self.mandatory_fields = mandatory_fields
-        self._update_relation()
-
-    def get_data_from_unit(self, key: str):
-        if not self.relation:
-            # This update relation doesn't seem to be needed, but I added it because apparently
-            # the data is empty in the unit tests.
-            # In reality, the constructor is called in every hook.
-            # In the unit tests when doing an update_relation_data, apparently it is not called.
-            self._update_relation()
-        if self.relation:
-            for unit in self.relation.units:
-                data = self.relation.data[unit].get(key)
-                if data:
-                    return data
-
-    def get_data_from_app(self, key: str):
-        if not self.relation or self.relation.app not in self.relation.data:
-            # This update relation doesn't seem to be needed, but I added it because apparently
-            # the data is empty in the unit tests.
-            # In reality, the constructor is called in every hook.
-            # In the unit tests when doing an update_relation_data, apparently it is not called.
-            self._update_relation()
-        if self.relation and self.relation.app in self.relation.data:
-            data = self.relation.data[self.relation.app].get(key)
-            if data:
-                return data
-
-    def is_missing_data_in_unit(self):
-        return not all([self.get_data_from_unit(field) for field in self.mandatory_fields])
-
-    def is_missing_data_in_app(self):
-        return not all([self.get_data_from_app(field) for field in self.mandatory_fields])
-
-    def _update_relation(self):
-        self.relation = self.framework.model.get_relation(self.relation_name)
-
-
-class MongoClient(BaseRelationClient):
-    """Requires side of a Mongo Endpoint"""
-
-    mandatory_fields_mapping = {
-        "reactive": ["connection_string"],
-        "ops": ["replica_set_uri", "replica_set_name"],
-    }
-
-    def __init__(self, charm: ops.charm.CharmBase, relation_name: str):
-        super().__init__(charm, relation_name, mandatory_fields=[])
-
-    @property
-    def connection_string(self):
-        if self.is_opts():
-            replica_set_uri = self.get_data_from_unit("replica_set_uri")
-            replica_set_name = self.get_data_from_unit("replica_set_name")
-            return f"{replica_set_uri}?replicaSet={replica_set_name}"
-        else:
-            return self.get_data_from_unit("connection_string")
-
-    def is_opts(self):
-        return not self.is_missing_data_in_unit_ops()
-
-    def is_missing_data_in_unit(self):
-        return self.is_missing_data_in_unit_ops() and self.is_missing_data_in_unit_reactive()
-
-    def is_missing_data_in_unit_ops(self):
-        return not all(
-            [self.get_data_from_unit(field) for field in self.mandatory_fields_mapping["ops"]]
-        )
-
-    def is_missing_data_in_unit_reactive(self):
-        return not all(
-            [self.get_data_from_unit(field) for field in self.mandatory_fields_mapping["reactive"]]
-        )
-
-
-class MysqlClient(BaseRelationClient):
-    """Requires side of a Mysql Endpoint"""
-
-    mandatory_fields = ["host", "port", "user", "password", "root_password"]
-
-    def __init__(self, charm: ops.charm.CharmBase, relation_name: str):
-        super().__init__(charm, relation_name, self.mandatory_fields)
-
-    @property
-    def host(self):
-        return self.get_data_from_unit("host")
-
-    @property
-    def port(self):
-        return self.get_data_from_unit("port")
-
-    @property
-    def user(self):
-        return self.get_data_from_unit("user")
-
-    @property
-    def password(self):
-        return self.get_data_from_unit("password")
-
-    @property
-    def root_password(self):
-        return self.get_data_from_unit("root_password")
-
-    @property
-    def database(self):
-        return self.get_data_from_unit("database")
-
-    def get_root_uri(self, database: str):
-        """
-        Get the URI for the mysql connection with the root user credentials
-        :param: database: Database name
-        :return: A string with the following format:
-                    mysql://root:<root_password>@<mysql_host>:<mysql_port>/<database>
-        """
-        return "mysql://root:{}@{}:{}/{}".format(
-            self.root_password, self.host, self.port, database
-        )
-
-    def get_uri(self):
-        """
-        Get the URI for the mysql connection with the standard user credentials
-        :param: database: Database name
-        :return: A string with the following format:
-                    mysql://<user>:<password>@<mysql_host>:<mysql_port>/<database>
-        """
-        return "mysql://{}:{}@{}:{}/{}".format(
-            self.user, self.password, self.host, self.port, self.database
-        )
diff --git a/installers/charm/osm-pol/tests/integration/test_charm.py b/installers/charm/osm-pol/tests/integration/test_charm.py
deleted file mode 100644 (file)
index 9210000..0000000
+++ /dev/null
@@ -1,170 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-# Learn more about testing at: https://juju.is/docs/sdk/testing
-
-import asyncio
-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())
-POL_APP = METADATA["name"]
-KAFKA_CHARM = "kafka-k8s"
-KAFKA_APP = "kafka"
-MONGO_DB_CHARM = "mongodb-k8s"
-MONGO_DB_APP = "mongodb"
-MARIADB_CHARM = "charmed-osm-mariadb-k8s"
-MARIADB_APP = "mariadb"
-ZOOKEEPER_CHARM = "zookeeper-k8s"
-ZOOKEEPER_APP = "zookeeper"
-APPS = [KAFKA_APP, ZOOKEEPER_APP, MONGO_DB_APP, MARIADB_APP, POL_APP]
-
-
-@pytest.mark.abort_on_fail
-async def test_pol_is_deployed(ops_test: OpsTest):
-    charm = await ops_test.build_charm(".")
-    resources = {"pol-image": METADATA["resources"]["pol-image"]["upstream-source"]}
-
-    await asyncio.gather(
-        ops_test.model.deploy(
-            charm, resources=resources, application_name=POL_APP, series="jammy"
-        ),
-        ops_test.model.deploy(KAFKA_CHARM, application_name=KAFKA_APP, channel="stable"),
-        ops_test.model.deploy(MONGO_DB_CHARM, application_name=MONGO_DB_APP, channel="5/edge"),
-        ops_test.model.deploy(MARIADB_CHARM, application_name=MARIADB_APP, channel="stable"),
-        ops_test.model.deploy(ZOOKEEPER_CHARM, application_name=ZOOKEEPER_APP, channel="stable"),
-    )
-
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS,
-        )
-    assert ops_test.model.applications[POL_APP].status == "blocked"
-    unit = ops_test.model.applications[POL_APP].units[0]
-    assert unit.workload_status_message == "need kafka, mongodb, mysql relations"
-
-    logger.info("Adding relations for other components")
-    await ops_test.model.add_relation(KAFKA_APP, ZOOKEEPER_APP)
-
-    logger.info("Adding relations for POL")
-    await ops_test.model.add_relation(POL_APP, KAFKA_APP)
-    await ops_test.model.add_relation(
-        "{}:mongodb".format(POL_APP), "{}:database".format(MONGO_DB_APP)
-    )
-    await ops_test.model.add_relation(POL_APP, MARIADB_APP)
-
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS,
-            status="active",
-        )
-
-
-@pytest.mark.abort_on_fail
-async def test_pol_scales_up(ops_test: OpsTest):
-    logger.info("Scaling up osm-pol")
-    expected_units = 3
-    assert len(ops_test.model.applications[POL_APP].units) == 1
-    await ops_test.model.applications[POL_APP].scale(expected_units)
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=[POL_APP], status="active", wait_for_exact_units=expected_units
-        )
-
-
-@pytest.mark.abort_on_fail
-@pytest.mark.parametrize("relation_to_remove", [KAFKA_APP, MONGO_DB_APP, MARIADB_APP])
-async def test_pol_blocks_without_relation(ops_test: OpsTest, relation_to_remove):
-    logger.info("Removing relation: %s", relation_to_remove)
-    # mongoDB relation is named "database"
-    local_relation = relation_to_remove
-    if relation_to_remove == MONGO_DB_APP:
-        local_relation = "database"
-    # mariaDB relation is named "mysql"
-    if relation_to_remove == MARIADB_APP:
-        local_relation = "mysql"
-    await asyncio.gather(
-        ops_test.model.applications[relation_to_remove].remove_relation(local_relation, POL_APP)
-    )
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(apps=[POL_APP])
-    assert ops_test.model.applications[POL_APP].status == "blocked"
-    for unit in ops_test.model.applications[POL_APP].units:
-        assert (
-            unit.workload_status_message
-            == f"need {'mysql' if relation_to_remove == MARIADB_APP else relation_to_remove} relation"
-        )
-    await ops_test.model.add_relation(POL_APP, relation_to_remove)
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS,
-            status="active",
-        )
-
-
-@pytest.mark.abort_on_fail
-async def test_pol_action_debug_mode_disabled(ops_test: OpsTest):
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS,
-            status="active",
-        )
-    logger.info("Running action 'get-debug-mode-information'")
-    action = (
-        await ops_test.model.applications[POL_APP]
-        .units[0]
-        .run_action("get-debug-mode-information")
-    )
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(apps=[POL_APP])
-    status = await ops_test.model.get_action_status(uuid_or_prefix=action.entity_id)
-    assert status[action.entity_id] == "failed"
-
-
-@pytest.mark.abort_on_fail
-async def test_pol_action_debug_mode_enabled(ops_test: OpsTest):
-    await ops_test.model.applications[POL_APP].set_config({"debug-mode": "true"})
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS,
-            status="active",
-        )
-    logger.info("Running action 'get-debug-mode-information'")
-    # list of units is not ordered
-    unit_id = list(
-        filter(
-            lambda x: (x.entity_id == f"{POL_APP}/0"), ops_test.model.applications[POL_APP].units
-        )
-    )[0]
-    action = await unit_id.run_action("get-debug-mode-information")
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(apps=[POL_APP])
-    status = await ops_test.model.get_action_status(uuid_or_prefix=action.entity_id)
-    message = await ops_test.model.get_action_output(action_uuid=action.entity_id)
-    assert status[action.entity_id] == "completed"
-    assert "command" in message
-    assert "password" in message
diff --git a/installers/charm/osm-pol/tests/unit/test_charm.py b/installers/charm/osm-pol/tests/unit/test_charm.py
deleted file mode 100644 (file)
index 1b5013a..0000000
+++ /dev/null
@@ -1,98 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-# Learn more about testing at: https://juju.is/docs/sdk/testing
-
-import pytest
-from ops.model import ActiveStatus, BlockedStatus
-from ops.testing import Harness
-from pytest_mock import MockerFixture
-
-from charm import CharmError, OsmPolCharm, check_service_active
-
-container_name = "pol"
-service_name = "pol"
-
-
-@pytest.fixture
-def harness(mocker: MockerFixture):
-    harness = Harness(OsmPolCharm)
-    harness.begin()
-    harness.container_pebble_ready(container_name)
-    yield harness
-    harness.cleanup()
-
-
-def test_missing_relations(harness: Harness):
-    harness.charm.on.config_changed.emit()
-    assert type(harness.charm.unit.status) == BlockedStatus
-    assert all(
-        relation in harness.charm.unit.status.message for relation in ["mongodb", "kafka", "mysql"]
-    )
-
-
-def test_ready(harness: Harness):
-    _add_relations(harness)
-    assert harness.charm.unit.status == ActiveStatus()
-
-
-def test_container_stops_after_relation_broken(harness: Harness):
-    harness.charm.on[container_name].pebble_ready.emit(container_name)
-    container = harness.charm.unit.get_container(container_name)
-    relation_ids = _add_relations(harness)
-    check_service_active(container, service_name)
-    harness.remove_relation(relation_ids[0])
-    with pytest.raises(CharmError):
-        check_service_active(container, service_name)
-
-
-def _add_relations(harness: Harness):
-    relation_ids = []
-    # Add mongo relation
-    relation_id = harness.add_relation("mongodb", "mongodb")
-    harness.add_relation_unit(relation_id, "mongodb/0")
-    harness.update_relation_data(
-        relation_id,
-        "mongodb",
-        {"uris": "mongodb://:1234", "username": "user", "password": "password"},
-    )
-    relation_ids.append(relation_id)
-    # Add kafka relation
-    relation_id = harness.add_relation("kafka", "kafka")
-    harness.add_relation_unit(relation_id, "kafka/0")
-    harness.update_relation_data(relation_id, "kafka", {"host": "kafka", "port": "9092"})
-    relation_ids.append(relation_id)
-    # Add mysql relation
-    relation_id = harness.add_relation("mysql", "mysql")
-    harness.add_relation_unit(relation_id, "mysql/0")
-    harness.update_relation_data(
-        relation_id,
-        "mysql/0",
-        {
-            "host": "mysql",
-            "port": "3306",
-            "user": "mano",
-            "password": "manopw",
-            "root_password": "rootmanopw",
-        },
-    )
-    relation_ids.append(relation_id)
-    return relation_ids
diff --git a/installers/charm/osm-pol/tox.ini b/installers/charm/osm-pol/tox.ini
deleted file mode 100644 (file)
index 2d95eca..0000000
+++ /dev/null
@@ -1,92 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-
-[tox]
-skipsdist=True
-skip_missing_interpreters = True
-envlist = lint, unit, integration
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-all_path = {[vars]src_path} {[vars]tst_path} 
-
-[testenv]
-basepython = python3.8
-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-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    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
-    # 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} \
-        -m pytest --ignore={[vars]tst_path}integration -v --tb native -s {posargs}
-    coverage report
-    coverage xml
-
-[testenv:integration]
-description = Run integration tests
-deps =
-    pytest
-    juju<3
-    pytest-operator
-    -r{toxinidir}/requirements.txt
-commands =
-    pytest -v --tb native --ignore={[vars]tst_path}unit --log-cli-level=INFO -s {posargs} --cloud microk8s
diff --git a/installers/charm/osm-ro/.gitignore b/installers/charm/osm-ro/.gitignore
deleted file mode 100644 (file)
index 87d0a58..0000000
+++ /dev/null
@@ -1,29 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-venv/
-build/
-*.charm
-.tox/
-.coverage
-coverage.xml
-__pycache__/
-*.py[cod]
-.vscode
\ No newline at end of file
diff --git a/installers/charm/osm-ro/.jujuignore b/installers/charm/osm-ro/.jujuignore
deleted file mode 100644 (file)
index 17c7a8b..0000000
+++ /dev/null
@@ -1,23 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-/venv
-*.py[cod]
-*.charm
diff --git a/installers/charm/osm-ro/CONTRIBUTING.md b/installers/charm/osm-ro/CONTRIBUTING.md
deleted file mode 100644 (file)
index 61f2a0a..0000000
+++ /dev/null
@@ -1,78 +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.
-
-For those usages not covered by the Apache License, Version 2.0 please
-contact: legal@canonical.com
-
-To get in touch with the maintainers, please contact:
-osm-charmers@lists.launchpad.net -->
-
-# Contributing
-
-## Overview
-
-This documents explains the processes and practices recommended for contributing enhancements to
-this operator.
-
-- Generally, before developing enhancements to this charm, you should consider [opening an issue
-  ](https://osm.etsi.org/bugzilla/enter_bug.cgi?product=OSM) explaining your use case. (Component=devops, version=master)
-- If you would like to chat with us about your use-cases or proposed implementation, you can reach
-  us at [OSM Juju public channel](https://opensourcemano.slack.com/archives/C027KJGPECA).
-- 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 gerrit patch onto
-  the `master` branch.
-
-## 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 dev
-# Enable DEBUG logging
-juju model-config logging-config="<root>=INFO;unit=DEBUG"
-# Deploy the charm
-juju deploy ./osm-ro_ubuntu-22.04-amd64.charm \
-    --resource ro-image=opensourcemano/ro:testing-daily --series jammy
-```
diff --git a/installers/charm/osm-ro/LICENSE b/installers/charm/osm-ro/LICENSE
deleted file mode 100644 (file)
index 7e9d504..0000000
+++ /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 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.
diff --git a/installers/charm/osm-ro/README.md b/installers/charm/osm-ro/README.md
deleted file mode 100644 (file)
index 44250f9..0000000
+++ /dev/null
@@ -1,42 +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.
-
-For those usages not covered by the Apache License, Version 2.0 please
-contact: legal@canonical.com
-
-To get in touch with the maintainers, please contact:
-osm-charmers@lists.launchpad.net -->
-
-<!-- 
-Avoid using this README file for information that is maintained or published elsewhere, e.g.:
-
-* metadata.yaml > published on Charmhub
-* documentation > published on (or linked to from) Charmhub
-* detailed contribution guide > documentation or CONTRIBUTING.md
-
-Use links instead. 
--->
-
-# OSM RO
-
-Charmhub package name: osm-ro
-More information: https://charmhub.io/osm-ro
-
-## Other resources
-
-* [Read more](https://osm.etsi.org/docs/user-guide/latest/) 
-
-* [Contributing](https://osm.etsi.org/gitweb/?p=osm/devops.git;a=blob;f=installers/charm/osm-ro/CONTRIBUTING.md)
-
-* See the [Juju SDK documentation](https://juju.is/docs/sdk) for more information about developing and improving charms.
diff --git a/installers/charm/osm-ro/actions.yaml b/installers/charm/osm-ro/actions.yaml
deleted file mode 100644 (file)
index 0d73468..0000000
+++ /dev/null
@@ -1,26 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Actions tab on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
-
-get-debug-mode-information:
-  description: Get information to debug the container
diff --git a/installers/charm/osm-ro/charmcraft.yaml b/installers/charm/osm-ro/charmcraft.yaml
deleted file mode 100644 (file)
index f5e3ff3..0000000
+++ /dev/null
@@ -1,36 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-
-type: charm
-bases:
-  - build-on:
-      - name: "ubuntu"
-        channel: "22.04"
-    run-on:
-      - name: "ubuntu"
-        channel: "22.04"
-
-parts:
-  charm:
-    # build-packages:
-    #   - git
-    prime:
-      - files/*
diff --git a/installers/charm/osm-ro/config.yaml b/installers/charm/osm-ro/config.yaml
deleted file mode 100644 (file)
index 036eecd..0000000
+++ /dev/null
@@ -1,103 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Configure tab on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
-
-options:
-  log-level:
-    default: "INFO"
-    description: |
-      Set the Logging Level.
-
-      Options:
-        - TRACE
-        - DEBUG
-        - INFO
-        - WARN
-        - ERROR
-        - FATAL
-    type: string
-  database-commonkey:
-    description: Database COMMON KEY
-    type: string
-    default: osm
-  certificates:
-    type: string
-    description: |
-      comma-separated list of <name>:<content> certificates.
-      Where:
-        name: name of the file for the certificate
-        content: base64 content of the certificate
-      The path for the files is /certs.
-
-  # Debug-mode options
-  debug-mode:
-    type: boolean
-    description: |
-      Great for OSM Developers! (Not recommended for production deployments)
-        
-      This action activates the Debug Mode, which sets up the container to be ready for debugging.
-      As part of the setup, SSH is enabled and a VSCode workspace file is automatically populated.
-
-      After enabling the debug-mode, execute the following command to get the information you need
-      to start debugging:
-        `juju run-action <unit name> get-debug-mode-information --wait`
-      
-      The previous command returns the command you need to execute, and the SSH password that was set.
-
-      See also:
-        - https://charmhub.io/osm-ro/configure#ro-hostpath
-        - https://charmhub.io/osm-ro/configure#common-hostpath
-    default: false
-  ro-hostpath:
-    type: string
-    description: |
-      Set this config to the local path of the ro module to persist the changes done during the
-      debug-mode session.
-
-      Example:
-        $ git clone "https://osm.etsi.org/gerrit/osm/RO" /home/ubuntu/ro
-        $ juju config ro ro-hostpath=/home/ubuntu/ro
-
-      This configuration only applies if option `debug-mode` is set to true. 
-
-  common-hostpath:
-    type: string
-    description: |
-      Set this config to the local path of the common module to persist the changes done during the
-      debug-mode session.
-
-      Example:
-        $ git clone "https://osm.etsi.org/gerrit/osm/common" /home/ubuntu/common
-        $ juju config ro common-hostpath=/home/ubuntu/common
-
-      This configuration only applies if option `debug-mode` is set to true.
-
-  period_refresh_active:
-    type: int
-    description: |
-      Updates the VNF status from VIM for every given period of time seconds.
-      Values equal or greater than 60 is allowed.
-      Disable the updates from VIM by setting -1.
-      Example:
-        $ juju config ro period_refresh_active=-1
-        $ juju config ro period_refresh_active=100
diff --git a/installers/charm/osm-ro/files/vscode-workspace.json b/installers/charm/osm-ro/files/vscode-workspace.json
deleted file mode 100644 (file)
index 5ab0913..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-{
-    "folders": [
-        {"path": "/usr/lib/python3/dist-packages/osm_ng_ro"},
-        {"path": "/usr/lib/python3/dist-packages/osm_common"},
-        {"path": "/usr/lib/python3/dist-packages/osm_ro_plugin"},
-        {"path": "/usr/lib/python3/dist-packages/osm_rosdn_arista_cloudvision"},
-        {"path": "/usr/lib/python3/dist-packages/osm_rosdn_dpb"},
-        {"path": "/usr/lib/python3/dist-packages/osm_rosdn_dynpac"},
-        {"path": "/usr/lib/python3/dist-packages/osm_rosdn_floodlightof"},
-        {"path": "/usr/lib/python3/dist-packages/osm_rosdn_ietfl2vpn"},
-        {"path": "/usr/lib/python3/dist-packages/osm_rosdn_juniper_contrail"},
-        {"path": "/usr/lib/python3/dist-packages/osm_rosdn_odlof"},
-        {"path": "/usr/lib/python3/dist-packages/osm_rosdn_onos_vpls"},
-        {"path": "/usr/lib/python3/dist-packages/osm_rosdn_onosof"},
-        {"path": "/usr/lib/python3/dist-packages/osm_rovim_aws"},
-        {"path": "/usr/lib/python3/dist-packages/osm_rovim_azure"},
-        {"path": "/usr/lib/python3/dist-packages/osm_rovim_gcp"},
-        {"path": "/usr/lib/python3/dist-packages/osm_rovim_openstack"},
-        {"path": "/usr/lib/python3/dist-packages/osm_rovim_vmware"},
-    ],
-    "launch": {
-        "configurations": [
-            {
-                "module": "osm_ng_ro.ro_main",
-                "name": "NG RO",
-                "request": "launch",
-                "type": "python",
-                "justMyCode": false,
-            }
-        ],
-        "version": "0.2.0",
-    },
-    "settings": {},
-}
\ No newline at end of file
diff --git a/installers/charm/osm-ro/lib/charms/data_platform_libs/v0/data_interfaces.py b/installers/charm/osm-ro/lib/charms/data_platform_libs/v0/data_interfaces.py
deleted file mode 100644 (file)
index b3da5aa..0000000
+++ /dev/null
@@ -1,1130 +0,0 @@
-# Copyright 2023 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.
-
-"""Library to manage the relation for the data-platform products.
-
-This library contains the Requires and Provides classes for handling the relation
-between an application and multiple managed application supported by the data-team:
-MySQL, Postgresql, MongoDB, Redis,  and Kakfa.
-
-### Database (MySQL, Postgresql, MongoDB, and Redis)
-
-#### Requires Charm
-This library is a uniform interface to a selection of common database
-metadata, with added custom events that add convenience to database management,
-and methods to consume the application related data.
-
-
-Following an example of using the DatabaseCreatedEvent, in the context of the
-application charm code:
-
-```python
-
-from charms.data_platform_libs.v0.data_interfaces import (
-    DatabaseCreatedEvent,
-    DatabaseRequires,
-)
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Charm events defined in the database requires charm library.
-        self.database = DatabaseRequires(self, relation_name="database", database_name="database")
-        self.framework.observe(self.database.on.database_created, self._on_database_created)
-
-    def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-
-        # Start application with rendered configuration
-        self._start_application(config_file)
-
-        # Set active status
-        self.unit.status = ActiveStatus("received database credentials")
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
--  database_created: event emitted when the requested database is created.
--  endpoints_changed: event emitted when the read/write endpoints of the database have changed.
--  read_only_endpoints_changed: event emitted when the read-only endpoints of the database
-  have changed. Event is not triggered if read/write endpoints changed too.
-
-If it is needed to connect multiple database clusters to the same relation endpoint
-the application charm can implement the same code as if it would connect to only
-one database cluster (like the above code example).
-
-To differentiate multiple clusters connected to the same relation endpoint
-the application charm can use the name of the remote application:
-
-```python
-
-def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
-    # Get the remote app name of the cluster that triggered this event
-    cluster = event.relation.app.name
-```
-
-It is also possible to provide an alias for each different database cluster/relation.
-
-So, it is possible to differentiate the clusters in two ways.
-The first is to use the remote application name, i.e., `event.relation.app.name`, as above.
-
-The second way is to use different event handlers to handle each cluster events.
-The implementation would be something like the following code:
-
-```python
-
-from charms.data_platform_libs.v0.data_interfaces import (
-    DatabaseCreatedEvent,
-    DatabaseRequires,
-)
-
-class ApplicationCharm(CharmBase):
-    # Application charm that connects to database charms.
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Define the cluster aliases and one handler for each cluster database created event.
-        self.database = DatabaseRequires(
-            self,
-            relation_name="database",
-            database_name="database",
-            relations_aliases = ["cluster1", "cluster2"],
-        )
-        self.framework.observe(
-            self.database.on.cluster1_database_created, self._on_cluster1_database_created
-        )
-        self.framework.observe(
-            self.database.on.cluster2_database_created, self._on_cluster2_database_created
-        )
-
-    def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster1
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-    def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None:
-        # Handle the created database on the cluster named cluster2
-
-        # Create configuration file for app
-        config_file = self._render_app_config_file(
-            event.username,
-            event.password,
-            event.endpoints,
-        )
-        ...
-
-```
-
-### Provider Charm
-
-Following an example of using the DatabaseRequestedEvent, in the context of the
-database charm code:
-
-```python
-from charms.data_platform_libs.v0.data_interfaces import DatabaseProvides
-
-class SampleCharm(CharmBase):
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        # Charm events defined in the database provides charm library.
-        self.provided_database = DatabaseProvides(self, relation_name="database")
-        self.framework.observe(self.provided_database.on.database_requested,
-            self._on_database_requested)
-        # Database generic helper
-        self.database = DatabaseHelper()
-
-    def _on_database_requested(self, event: DatabaseRequestedEvent) -> None:
-        # Handle the event triggered by a new database requested in the relation
-        # Retrieve the database name using the charm library.
-        db_name = event.database
-        # generate a new user credential
-        username = self.database.generate_user()
-        password = self.database.generate_password()
-        # set the credentials for the relation
-        self.provided_database.set_credentials(event.relation.id, username, password)
-        # set other variables for the relation event.set_tls("False")
-```
-As shown above, the library provides a custom event (database_requested) to handle
-the situation when an application charm requests a new database to be created.
-It's preferred to subscribe to this event instead of relation changed event to avoid
-creating a new database when other information other than a database name is
-exchanged in the relation databag.
-
-### Kafka
-
-This library is the interface to use and interact with the Kafka charm. This library contains
-custom events that add convenience to manage Kafka, and provides methods to consume the
-application related data.
-
-#### Requirer Charm
-
-```python
-
-from charms.data_platform_libs.v0.data_interfaces import (
-    BootstrapServerChangedEvent,
-    KafkaRequires,
-    TopicCreatedEvent,
-)
-
-class ApplicationCharm(CharmBase):
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.kafka = KafkaRequires(self, "kafka_client", "test-topic")
-        self.framework.observe(
-            self.kafka.on.bootstrap_server_changed, self._on_kafka_bootstrap_server_changed
-        )
-        self.framework.observe(
-            self.kafka.on.topic_created, self._on_kafka_topic_created
-        )
-
-    def _on_kafka_bootstrap_server_changed(self, event: BootstrapServerChangedEvent):
-        # Event triggered when a bootstrap server was changed for this application
-
-        new_bootstrap_server = event.bootstrap_server
-        ...
-
-    def _on_kafka_topic_created(self, event: TopicCreatedEvent):
-        # Event triggered when a topic was created for this application
-        username = event.username
-        password = event.password
-        tls = event.tls
-        tls_ca= event.tls_ca
-        bootstrap_server event.bootstrap_server
-        consumer_group_prefic = event.consumer_group_prefix
-        zookeeper_uris = event.zookeeper_uris
-        ...
-
-```
-
-As shown above, the library provides some custom events to handle specific situations,
-which are listed below:
-
-- topic_created: event emitted when the requested topic is created.
-- bootstrap_server_changed: event emitted when the bootstrap server have changed.
-- credential_changed: event emitted when the credentials of Kafka changed.
-
-### Provider Charm
-
-Following the previous example, this is an example of the provider charm.
-
-```python
-class SampleCharm(CharmBase):
-
-from charms.data_platform_libs.v0.data_interfaces import (
-    KafkaProvides,
-    TopicRequestedEvent,
-)
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Default charm events.
-        self.framework.observe(self.on.start, self._on_start)
-
-        # Charm events defined in the Kafka Provides charm library.
-        self.kafka_provider = KafkaProvides(self, relation_name="kafka_client")
-        self.framework.observe(self.kafka_provider.on.topic_requested, self._on_topic_requested)
-        # Kafka generic helper
-        self.kafka = KafkaHelper()
-
-    def _on_topic_requested(self, event: TopicRequestedEvent):
-        # Handle the on_topic_requested event.
-
-        topic = event.topic
-        relation_id = event.relation.id
-        # set connection info in the databag relation
-        self.kafka_provider.set_bootstrap_server(relation_id, self.kafka.get_bootstrap_server())
-        self.kafka_provider.set_credentials(relation_id, username=username, password=password)
-        self.kafka_provider.set_consumer_group_prefix(relation_id, ...)
-        self.kafka_provider.set_tls(relation_id, "False")
-        self.kafka_provider.set_zookeeper_uris(relation_id, ...)
-
-```
-As shown above, the library provides a custom event (topic_requested) to handle
-the situation when an application charm requests a new topic to be created.
-It is preferred to subscribe to this event instead of relation changed event to avoid
-creating a new topic when other information other than a topic name is
-exchanged in the relation databag.
-"""
-
-import json
-import logging
-from abc import ABC, abstractmethod
-from collections import namedtuple
-from datetime import datetime
-from typing import List, Optional
-
-from ops.charm import (
-    CharmBase,
-    CharmEvents,
-    RelationChangedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-)
-from ops.framework import EventSource, Object
-from ops.model import Relation
-
-# The unique Charmhub library identifier, never change it
-LIBID = "6c3e6b6680d64e9c89e611d1a15f65be"
-
-# 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 = 7
-
-PYDEPS = ["ops>=2.0.0"]
-
-logger = logging.getLogger(__name__)
-
-Diff = namedtuple("Diff", "added changed deleted")
-Diff.__doc__ = """
-A tuple for storing the diff between two data mappings.
-
-added - keys that were added
-changed - keys that still exist but have new values
-deleted - key that were deleted"""
-
-
-def diff(event: RelationChangedEvent, bucket: str) -> Diff:
-    """Retrieves the diff of the data in the relation changed databag.
-
-    Args:
-        event: relation changed event.
-        bucket: bucket of the databag (app or unit)
-
-    Returns:
-        a Diff instance containing the added, deleted and changed
-            keys from the event relation databag.
-    """
-    # Retrieve the old data from the data key in the application relation databag.
-    old_data = json.loads(event.relation.data[bucket].get("data", "{}"))
-    # Retrieve the new data from the event relation databag.
-    new_data = {
-        key: value for key, value in event.relation.data[event.app].items() if key != "data"
-    }
-
-    # These are the keys that were added to the databag and triggered this event.
-    added = new_data.keys() - old_data.keys()
-    # These are the keys that were removed from the databag and triggered this event.
-    deleted = old_data.keys() - new_data.keys()
-    # These are the keys that already existed in the databag,
-    # but had their values changed.
-    changed = {key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]}
-    # Convert the new_data to a serializable format and save it for a next diff check.
-    event.relation.data[bucket].update({"data": json.dumps(new_data)})
-
-    # Return the diff with all possible changes.
-    return Diff(added, changed, deleted)
-
-
-# Base DataProvides and DataRequires
-
-
-class DataProvides(Object, ABC):
-    """Base provides-side of the data products relation."""
-
-    def __init__(self, charm: CharmBase, relation_name: str) -> None:
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.framework.observe(
-            charm.on[relation_name].relation_changed,
-            self._on_relation_changed,
-        )
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        return diff(event, self.local_app)
-
-    @abstractmethod
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the relation data has changed."""
-        raise NotImplementedError
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation id).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = {
-                key: value for key, value in relation.data[relation.app].items() if key != "data"
-            }
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return list(self.charm.model.relations[self.relation_name])
-
-    def set_credentials(self, relation_id: int, username: str, password: str) -> None:
-        """Set credentials.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            username: user that was created.
-            password: password of the created user.
-        """
-        self._update_relation_data(
-            relation_id,
-            {
-                "username": username,
-                "password": password,
-            },
-        )
-
-    def set_tls(self, relation_id: int, tls: str) -> None:
-        """Set whether TLS is enabled.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            tls: whether tls is enabled (True or False).
-        """
-        self._update_relation_data(relation_id, {"tls": tls})
-
-    def set_tls_ca(self, relation_id: int, tls_ca: str) -> None:
-        """Set the TLS CA in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            tls_ca: TLS certification authority.
-        """
-        self._update_relation_data(relation_id, {"tls_ca": tls_ca})
-
-
-class DataRequires(Object, ABC):
-    """Requires-side of the relation."""
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        extra_user_roles: str = None,
-    ):
-        """Manager of base client relations."""
-        super().__init__(charm, relation_name)
-        self.charm = charm
-        self.extra_user_roles = extra_user_roles
-        self.local_app = self.charm.model.app
-        self.local_unit = self.charm.unit
-        self.relation_name = relation_name
-        self.framework.observe(
-            self.charm.on[relation_name].relation_joined, self._on_relation_joined_event
-        )
-        self.framework.observe(
-            self.charm.on[relation_name].relation_changed, self._on_relation_changed_event
-        )
-
-    @abstractmethod
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the relation."""
-        raise NotImplementedError
-
-    @abstractmethod
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        raise NotImplementedError
-
-    def fetch_relation_data(self) -> dict:
-        """Retrieves data from relation.
-
-        This function can be used to retrieve data from a relation
-        in the charm code when outside an event callback.
-        Function cannot be used in `*-relation-broken` events and will raise an exception.
-
-        Returns:
-            a dict of the values stored in the relation data bag
-                for all relation instances (indexed by the relation ID).
-        """
-        data = {}
-        for relation in self.relations:
-            data[relation.id] = {
-                key: value for key, value in relation.data[relation.app].items() if key != "data"
-            }
-        return data
-
-    def _update_relation_data(self, relation_id: int, data: dict) -> None:
-        """Updates a set of key-value pairs in the relation.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            data: dict containing the key-value pairs
-                that should be updated in the relation.
-        """
-        if self.local_unit.is_leader():
-            relation = self.charm.model.get_relation(self.relation_name, relation_id)
-            relation.data[self.local_app].update(data)
-
-    def _diff(self, event: RelationChangedEvent) -> Diff:
-        """Retrieves the diff of the data in the relation changed databag.
-
-        Args:
-            event: relation changed event.
-
-        Returns:
-            a Diff instance containing the added, deleted and changed
-                keys from the event relation databag.
-        """
-        return diff(event, self.local_unit)
-
-    @property
-    def relations(self) -> List[Relation]:
-        """The list of Relation instances associated with this relation_name."""
-        return [
-            relation
-            for relation in self.charm.model.relations[self.relation_name]
-            if self._is_relation_active(relation)
-        ]
-
-    @staticmethod
-    def _is_relation_active(relation: Relation):
-        try:
-            _ = repr(relation.data)
-            return True
-        except RuntimeError:
-            return False
-
-    @staticmethod
-    def _is_resource_created_for_relation(relation: Relation):
-        return (
-            "username" in relation.data[relation.app] and "password" in relation.data[relation.app]
-        )
-
-    def is_resource_created(self, relation_id: Optional[int] = None) -> bool:
-        """Check if the resource has been created.
-
-        This function can be used to check if the Provider answered with data in the charm code
-        when outside an event callback.
-
-        Args:
-            relation_id (int, optional): When provided the check is done only for the relation id
-                provided, otherwise the check is done for all relations
-
-        Returns:
-            True or False
-
-        Raises:
-            IndexError: If relation_id is provided but that relation does not exist
-        """
-        if relation_id is not None:
-            try:
-                relation = [relation for relation in self.relations if relation.id == relation_id][
-                    0
-                ]
-                return self._is_resource_created_for_relation(relation)
-            except IndexError:
-                raise IndexError(f"relation id {relation_id} cannot be accessed")
-        else:
-            return (
-                all(
-                    [
-                        self._is_resource_created_for_relation(relation)
-                        for relation in self.relations
-                    ]
-                )
-                if self.relations
-                else False
-            )
-
-
-# General events
-
-
-class ExtraRoleEvent(RelationEvent):
-    """Base class for data events."""
-
-    @property
-    def extra_user_roles(self) -> Optional[str]:
-        """Returns the extra user roles that were requested."""
-        return self.relation.data[self.relation.app].get("extra-user-roles")
-
-
-class AuthenticationEvent(RelationEvent):
-    """Base class for authentication fields for events."""
-
-    @property
-    def username(self) -> Optional[str]:
-        """Returns the created username."""
-        return self.relation.data[self.relation.app].get("username")
-
-    @property
-    def password(self) -> Optional[str]:
-        """Returns the password for the created user."""
-        return self.relation.data[self.relation.app].get("password")
-
-    @property
-    def tls(self) -> Optional[str]:
-        """Returns whether TLS is configured."""
-        return self.relation.data[self.relation.app].get("tls")
-
-    @property
-    def tls_ca(self) -> Optional[str]:
-        """Returns TLS CA."""
-        return self.relation.data[self.relation.app].get("tls-ca")
-
-
-# Database related events and fields
-
-
-class DatabaseProvidesEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def database(self) -> Optional[str]:
-        """Returns the database that was requested."""
-        return self.relation.data[self.relation.app].get("database")
-
-
-class DatabaseRequestedEvent(DatabaseProvidesEvent, ExtraRoleEvent):
-    """Event emitted when a new database is requested for use on this relation."""
-
-
-class DatabaseProvidesEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_requested = EventSource(DatabaseRequestedEvent)
-
-
-class DatabaseRequiresEvent(RelationEvent):
-    """Base class for database events."""
-
-    @property
-    def endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read/write endpoints."""
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def read_only_endpoints(self) -> Optional[str]:
-        """Returns a comma separated list of read only endpoints."""
-        return self.relation.data[self.relation.app].get("read-only-endpoints")
-
-    @property
-    def replset(self) -> Optional[str]:
-        """Returns the replicaset name.
-
-        MongoDB only.
-        """
-        return self.relation.data[self.relation.app].get("replset")
-
-    @property
-    def uris(self) -> Optional[str]:
-        """Returns the connection URIs.
-
-        MongoDB, Redis, OpenSearch.
-        """
-        return self.relation.data[self.relation.app].get("uris")
-
-    @property
-    def version(self) -> Optional[str]:
-        """Returns the version of the database.
-
-        Version as informed by the database daemon.
-        """
-        return self.relation.data[self.relation.app].get("version")
-
-
-class DatabaseCreatedEvent(AuthenticationEvent, DatabaseRequiresEvent):
-    """Event emitted when a new database is created for use on this relation."""
-
-
-class DatabaseEndpointsChangedEvent(AuthenticationEvent, DatabaseRequiresEvent):
-    """Event emitted when the read/write endpoints are changed."""
-
-
-class DatabaseReadOnlyEndpointsChangedEvent(AuthenticationEvent, DatabaseRequiresEvent):
-    """Event emitted when the read only endpoints are changed."""
-
-
-class DatabaseRequiresEvents(CharmEvents):
-    """Database events.
-
-    This class defines the events that the database can emit.
-    """
-
-    database_created = EventSource(DatabaseCreatedEvent)
-    endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
-    read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent)
-
-
-# Database Provider and Requires
-
-
-class DatabaseProvides(DataProvides):
-    """Provider-side of the database relations."""
-
-    on = DatabaseProvidesEvents()
-
-    def __init__(self, charm: CharmBase, relation_name: str) -> None:
-        super().__init__(charm, relation_name)
-
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the relation has changed."""
-        # Only the leader should handle this event.
-        if not self.local_unit.is_leader():
-            return
-
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Emit a database requested event if the setup key (database name and optional
-        # extra user roles) was added to the relation databag by the application.
-        if "database" in diff.added:
-            self.on.database_requested.emit(event.relation, app=event.app, unit=event.unit)
-
-    def set_endpoints(self, relation_id: int, connection_strings: str) -> None:
-        """Set database primary connections.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            connection_strings: database hosts and ports comma separated list.
-        """
-        self._update_relation_data(relation_id, {"endpoints": connection_strings})
-
-    def set_read_only_endpoints(self, relation_id: int, connection_strings: str) -> None:
-        """Set database replicas connection strings.
-
-        This function writes in the application data bag, therefore,
-        only the leader unit can call it.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            connection_strings: database hosts and ports comma separated list.
-        """
-        self._update_relation_data(relation_id, {"read-only-endpoints": connection_strings})
-
-    def set_replset(self, relation_id: int, replset: str) -> None:
-        """Set replica set name in the application relation databag.
-
-        MongoDB only.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            replset: replica set name.
-        """
-        self._update_relation_data(relation_id, {"replset": replset})
-
-    def set_uris(self, relation_id: int, uris: str) -> None:
-        """Set the database connection URIs in the application relation databag.
-
-        MongoDB, Redis, and OpenSearch only.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            uris: connection URIs.
-        """
-        self._update_relation_data(relation_id, {"uris": uris})
-
-    def set_version(self, relation_id: int, version: str) -> None:
-        """Set the database version in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            version: database version.
-        """
-        self._update_relation_data(relation_id, {"version": version})
-
-
-class DatabaseRequires(DataRequires):
-    """Requires-side of the database relation."""
-
-    on = DatabaseRequiresEvents()
-
-    def __init__(
-        self,
-        charm,
-        relation_name: str,
-        database_name: str,
-        extra_user_roles: str = None,
-        relations_aliases: List[str] = None,
-    ):
-        """Manager of database client relations."""
-        super().__init__(charm, relation_name, extra_user_roles)
-        self.database = database_name
-        self.relations_aliases = relations_aliases
-
-        # Define custom event names for each alias.
-        if relations_aliases:
-            # Ensure the number of aliases does not exceed the maximum
-            # of connections allowed in the specific relation.
-            relation_connection_limit = self.charm.meta.requires[relation_name].limit
-            if len(relations_aliases) != relation_connection_limit:
-                raise ValueError(
-                    f"The number of aliases must match the maximum number of connections allowed in the relation. "
-                    f"Expected {relation_connection_limit}, got {len(relations_aliases)}"
-                )
-
-            for relation_alias in relations_aliases:
-                self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent)
-                self.on.define_event(
-                    f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent
-                )
-                self.on.define_event(
-                    f"{relation_alias}_read_only_endpoints_changed",
-                    DatabaseReadOnlyEndpointsChangedEvent,
-                )
-
-    def _assign_relation_alias(self, relation_id: int) -> None:
-        """Assigns an alias to a relation.
-
-        This function writes in the unit data bag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-        """
-        # If no aliases were provided, return immediately.
-        if not self.relations_aliases:
-            return
-
-        # Return if an alias was already assigned to this relation
-        # (like when there are more than one unit joining the relation).
-        if (
-            self.charm.model.get_relation(self.relation_name, relation_id)
-            .data[self.local_unit]
-            .get("alias")
-        ):
-            return
-
-        # Retrieve the available aliases (the ones that weren't assigned to any relation).
-        available_aliases = self.relations_aliases[:]
-        for relation in self.charm.model.relations[self.relation_name]:
-            alias = relation.data[self.local_unit].get("alias")
-            if alias:
-                logger.debug("Alias %s was already assigned to relation %d", alias, relation.id)
-                available_aliases.remove(alias)
-
-        # Set the alias in the unit relation databag of the specific relation.
-        relation = self.charm.model.get_relation(self.relation_name, relation_id)
-        relation.data[self.local_unit].update({"alias": available_aliases[0]})
-
-    def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None:
-        """Emit an aliased event to a particular relation if it has an alias.
-
-        Args:
-            event: the relation changed event that was received.
-            event_name: the name of the event to emit.
-        """
-        alias = self._get_relation_alias(event.relation.id)
-        if alias:
-            getattr(self.on, f"{alias}_{event_name}").emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-    def _get_relation_alias(self, relation_id: int) -> Optional[str]:
-        """Returns the relation alias.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-
-        Returns:
-            the relation alias or None if the relation was not found.
-        """
-        for relation in self.charm.model.relations[self.relation_name]:
-            if relation.id == relation_id:
-                return relation.data[self.local_unit].get("alias")
-        return None
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the database relation."""
-        # If relations aliases were provided, assign one to the relation.
-        self._assign_relation_alias(event.relation.id)
-
-        # Sets both database and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the database.
-        if self.extra_user_roles:
-            self._update_relation_data(
-                event.relation.id,
-                {
-                    "database": self.database,
-                    "extra-user-roles": self.extra_user_roles,
-                },
-            )
-        else:
-            self._update_relation_data(event.relation.id, {"database": self.database})
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the database relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the database is created
-        # (the database charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("database created at %s", datetime.now())
-            self.on.database_created.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "database_created")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “database_created“ is triggered.
-            return
-
-        # Emit an endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit)
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "endpoints_changed")
-
-            # To avoid unnecessary application restarts do not trigger
-            # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered.
-            return
-
-        # Emit a read only endpoints changed event if the database
-        # added or changed this info in the relation databag.
-        if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("read-only-endpoints changed on %s", datetime.now())
-            self.on.read_only_endpoints_changed.emit(
-                event.relation, app=event.app, unit=event.unit
-            )
-
-            # Emit the aliased event (if any).
-            self._emit_aliased_event(event, "read_only_endpoints_changed")
-
-
-# Kafka related events
-
-
-class KafkaProvidesEvent(RelationEvent):
-    """Base class for Kafka events."""
-
-    @property
-    def topic(self) -> Optional[str]:
-        """Returns the topic that was requested."""
-        return self.relation.data[self.relation.app].get("topic")
-
-
-class TopicRequestedEvent(KafkaProvidesEvent, ExtraRoleEvent):
-    """Event emitted when a new topic is requested for use on this relation."""
-
-
-class KafkaProvidesEvents(CharmEvents):
-    """Kafka events.
-
-    This class defines the events that the Kafka can emit.
-    """
-
-    topic_requested = EventSource(TopicRequestedEvent)
-
-
-class KafkaRequiresEvent(RelationEvent):
-    """Base class for Kafka events."""
-
-    @property
-    def bootstrap_server(self) -> Optional[str]:
-        """Returns a a comma-seperated list of broker uris."""
-        return self.relation.data[self.relation.app].get("endpoints")
-
-    @property
-    def consumer_group_prefix(self) -> Optional[str]:
-        """Returns the consumer-group-prefix."""
-        return self.relation.data[self.relation.app].get("consumer-group-prefix")
-
-    @property
-    def zookeeper_uris(self) -> Optional[str]:
-        """Returns a comma separated list of Zookeeper uris."""
-        return self.relation.data[self.relation.app].get("zookeeper-uris")
-
-
-class TopicCreatedEvent(AuthenticationEvent, KafkaRequiresEvent):
-    """Event emitted when a new topic is created for use on this relation."""
-
-
-class BootstrapServerChangedEvent(AuthenticationEvent, KafkaRequiresEvent):
-    """Event emitted when the bootstrap server is changed."""
-
-
-class KafkaRequiresEvents(CharmEvents):
-    """Kafka events.
-
-    This class defines the events that the Kafka can emit.
-    """
-
-    topic_created = EventSource(TopicCreatedEvent)
-    bootstrap_server_changed = EventSource(BootstrapServerChangedEvent)
-
-
-# Kafka Provides and Requires
-
-
-class KafkaProvides(DataProvides):
-    """Provider-side of the Kafka relation."""
-
-    on = KafkaProvidesEvents()
-
-    def __init__(self, charm: CharmBase, relation_name: str) -> None:
-        super().__init__(charm, relation_name)
-
-    def _on_relation_changed(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the relation has changed."""
-        # Only the leader should handle this event.
-        if not self.local_unit.is_leader():
-            return
-
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Emit a topic requested event if the setup key (topic name and optional
-        # extra user roles) was added to the relation databag by the application.
-        if "topic" in diff.added:
-            self.on.topic_requested.emit(event.relation, app=event.app, unit=event.unit)
-
-    def set_bootstrap_server(self, relation_id: int, bootstrap_server: str) -> None:
-        """Set the bootstrap server in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            bootstrap_server: the bootstrap server address.
-        """
-        self._update_relation_data(relation_id, {"endpoints": bootstrap_server})
-
-    def set_consumer_group_prefix(self, relation_id: int, consumer_group_prefix: str) -> None:
-        """Set the consumer group prefix in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            consumer_group_prefix: the consumer group prefix string.
-        """
-        self._update_relation_data(relation_id, {"consumer-group-prefix": consumer_group_prefix})
-
-    def set_zookeeper_uris(self, relation_id: int, zookeeper_uris: str) -> None:
-        """Set the zookeeper uris in the application relation databag.
-
-        Args:
-            relation_id: the identifier for a particular relation.
-            zookeeper_uris: comma-seperated list of ZooKeeper server uris.
-        """
-        self._update_relation_data(relation_id, {"zookeeper-uris": zookeeper_uris})
-
-
-class KafkaRequires(DataRequires):
-    """Requires-side of the Kafka relation."""
-
-    on = KafkaRequiresEvents()
-
-    def __init__(self, charm, relation_name: str, topic: str, extra_user_roles: str = None):
-        """Manager of Kafka client relations."""
-        # super().__init__(charm, relation_name)
-        super().__init__(charm, relation_name, extra_user_roles)
-        self.charm = charm
-        self.topic = topic
-
-    def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
-        """Event emitted when the application joins the Kafka relation."""
-        # Sets both topic and extra user roles in the relation
-        # if the roles are provided. Otherwise, sets only the topic.
-        self._update_relation_data(
-            event.relation.id,
-            {
-                "topic": self.topic,
-                "extra-user-roles": self.extra_user_roles,
-            }
-            if self.extra_user_roles is not None
-            else {"topic": self.topic},
-        )
-
-    def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
-        """Event emitted when the Kafka relation has changed."""
-        # Check which data has changed to emit customs events.
-        diff = self._diff(event)
-
-        # Check if the topic is created
-        # (the Kafka charm shared the credentials).
-        if "username" in diff.added and "password" in diff.added:
-            # Emit the default event (the one without an alias).
-            logger.info("topic created at %s", datetime.now())
-            self.on.topic_created.emit(event.relation, app=event.app, unit=event.unit)
-
-            # To avoid unnecessary application restarts do not trigger
-            # “endpoints_changed“ event if “topic_created“ is triggered.
-            return
-
-        # Emit an endpoints (bootstap-server) changed event if the Kakfa endpoints
-        # added or changed this info in the relation databag.
-        if "endpoints" in diff.added or "endpoints" in diff.changed:
-            # Emit the default event (the one without an alias).
-            logger.info("endpoints changed on %s", datetime.now())
-            self.on.bootstrap_server_changed.emit(
-                event.relation, app=event.app, unit=event.unit
-            )  # here check if this is the right design
-            return
diff --git a/installers/charm/osm-ro/lib/charms/kafka_k8s/v0/kafka.py b/installers/charm/osm-ro/lib/charms/kafka_k8s/v0/kafka.py
deleted file mode 100644 (file)
index aeb5edc..0000000
+++ /dev/null
@@ -1,200 +0,0 @@
-# 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.
-
-"""Kafka library.
-
-This [library](https://juju.is/docs/sdk/libraries) implements both sides of the
-`kafka` [interface](https://juju.is/docs/sdk/relations).
-
-The *provider* side of this interface is implemented by the
-[kafka-k8s Charmed Operator](https://charmhub.io/kafka-k8s).
-
-Any Charmed Operator that *requires* Kafka for providing its
-service should implement the *requirer* side of this interface.
-
-In a nutshell using this library to implement a Charmed Operator *requiring*
-Kafka would look like
-
-```
-$ charmcraft fetch-lib charms.kafka_k8s.v0.kafka
-```
-
-`metadata.yaml`:
-
-```
-requires:
-  kafka:
-    interface: kafka
-    limit: 1
-```
-
-`src/charm.py`:
-
-```
-from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
-from ops.charm import CharmBase
-
-
-class MyCharm(CharmBase):
-
-    on = KafkaEvents()
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.kafka = KafkaRequires(self)
-        self.framework.observe(
-            self.on.kafka_available,
-            self._on_kafka_available,
-        )
-        self.framework.observe(
-            self.on["kafka"].relation_broken,
-            self._on_kafka_broken,
-        )
-
-    def _on_kafka_available(self, event):
-        # Get Kafka host and port
-        host: str = self.kafka.host
-        port: int = self.kafka.port
-        # host => "kafka-k8s"
-        # port => 9092
-
-    def _on_kafka_broken(self, event):
-        # Stop service
-        # ...
-        self.unit.status = BlockedStatus("need kafka relation")
-```
-
-You can file bugs
-[here](https://github.com/charmed-osm/kafka-k8s-operator/issues)!
-"""
-
-from typing import Optional
-
-from ops.charm import CharmBase, CharmEvents
-from ops.framework import EventBase, EventSource, Object
-
-# The unique Charmhub library identifier, never change it
-from ops.model import Relation
-
-LIBID = "eacc8c85082347c9aae740e0220b8376"
-
-# 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 = 4
-
-
-KAFKA_HOST_APP_KEY = "host"
-KAFKA_PORT_APP_KEY = "port"
-
-
-class _KafkaAvailableEvent(EventBase):
-    """Event emitted when Kafka is available."""
-
-
-class KafkaEvents(CharmEvents):
-    """Kafka events.
-
-    This class defines the events that Kafka can emit.
-
-    Events:
-        kafka_available (_KafkaAvailableEvent)
-    """
-
-    kafka_available = EventSource(_KafkaAvailableEvent)
-
-
-class KafkaRequires(Object):
-    """Requires-side of the Kafka relation."""
-
-    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
-        super().__init__(charm, endpoint_name)
-        self.charm = charm
-        self._endpoint_name = endpoint_name
-
-        # Observe relation events
-        event_observe_mapping = {
-            charm.on[self._endpoint_name].relation_changed: self._on_relation_changed,
-        }
-        for event, observer in event_observe_mapping.items():
-            self.framework.observe(event, observer)
-
-    def _on_relation_changed(self, event) -> None:
-        if event.relation.app and all(
-            key in event.relation.data[event.relation.app]
-            for key in (KAFKA_HOST_APP_KEY, KAFKA_PORT_APP_KEY)
-        ):
-            self.charm.on.kafka_available.emit()
-
-    @property
-    def host(self) -> str:
-        """Get kafka hostname."""
-        relation: Relation = self.model.get_relation(self._endpoint_name)
-        return (
-            relation.data[relation.app].get(KAFKA_HOST_APP_KEY)
-            if relation and relation.app
-            else None
-        )
-
-    @property
-    def port(self) -> int:
-        """Get kafka port number."""
-        relation: Relation = self.model.get_relation(self._endpoint_name)
-        return (
-            int(relation.data[relation.app].get(KAFKA_PORT_APP_KEY))
-            if relation and relation.app
-            else None
-        )
-
-
-class KafkaProvides(Object):
-    """Provides-side of the Kafka relation."""
-
-    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
-        super().__init__(charm, endpoint_name)
-        self._endpoint_name = endpoint_name
-
-    def set_host_info(self, host: str, port: int, relation: Optional[Relation] = None) -> None:
-        """Set Kafka host and port.
-
-        This function writes in the application data of the relation, therefore,
-        only the unit leader can call it.
-
-        Args:
-            host (str): Kafka hostname or IP address.
-            port (int): Kafka port.
-            relation (Optional[Relation]): Relation to update.
-                                           If not specified, all relations will be updated.
-
-        Raises:
-            Exception: if a non-leader unit calls this function.
-        """
-        if not self.model.unit.is_leader():
-            raise Exception("only the leader set host information.")
-
-        if relation:
-            self._update_relation_data(host, port, relation)
-            return
-
-        for relation in self.model.relations[self._endpoint_name]:
-            self._update_relation_data(host, port, relation)
-
-    def _update_relation_data(self, host: str, port: int, relation: Relation) -> None:
-        """Update data in relation if needed."""
-        relation.data[self.model.app][KAFKA_HOST_APP_KEY] = host
-        relation.data[self.model.app][KAFKA_PORT_APP_KEY] = str(port)
diff --git a/installers/charm/osm-ro/lib/charms/observability_libs/v1/kubernetes_service_patch.py b/installers/charm/osm-ro/lib/charms/observability_libs/v1/kubernetes_service_patch.py
deleted file mode 100644 (file)
index 506dbf0..0000000
+++ /dev/null
@@ -1,291 +0,0 @@
-# Copyright 2021 Canonical Ltd.
-# See LICENSE file for licensing details.
-#   http://www.apache.org/licenses/LICENSE-2.0
-
-"""# KubernetesServicePatch Library.
-
-This library is designed to enable developers to more simply patch the Kubernetes Service created
-by Juju during the deployment of a sidecar charm. When sidecar charms are deployed, Juju creates a
-service named after the application in the namespace (named after the Juju model). This service by
-default contains a "placeholder" port, which is 65536/TCP.
-
-When modifying the default set of resources managed by Juju, one must consider the lifecycle of the
-charm. In this case, any modifications to the default service (created during deployment), will be
-overwritten during a charm upgrade.
-
-When initialised, this library binds a handler to the parent charm's `install` and `upgrade_charm`
-events which applies the patch to the cluster. This should ensure that the service ports are
-correct throughout the charm's life.
-
-The constructor simply takes a reference to the parent charm, and a list of
-[`lightkube`](https://github.com/gtsystem/lightkube) ServicePorts that each define a port for the
-service. For information regarding the `lightkube` `ServicePort` model, please visit the
-`lightkube` [docs](https://gtsystem.github.io/lightkube-models/1.23/models/core_v1/#serviceport).
-
-Optionally, a name of the service (in case service name needs to be patched as well), labels,
-selectors, and annotations can be provided as keyword arguments.
-
-## Getting Started
-
-To get started using the library, you just need to fetch the library using `charmcraft`. **Note
-that you also need to add `lightkube` and `lightkube-models` to your charm's `requirements.txt`.**
-
-```shell
-cd some-charm
-charmcraft fetch-lib charms.observability_libs.v0.kubernetes_service_patch
-echo <<-EOF >> requirements.txt
-lightkube
-lightkube-models
-EOF
-```
-
-Then, to initialise the library:
-
-For `ClusterIP` services:
-
-```python
-# ...
-from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
-from lightkube.models.core_v1 import ServicePort
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    port = ServicePort(443, name=f"{self.app.name}")
-    self.service_patcher = KubernetesServicePatch(self, [port])
-    # ...
-```
-
-For `LoadBalancer`/`NodePort` services:
-
-```python
-# ...
-from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
-from lightkube.models.core_v1 import ServicePort
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    port = ServicePort(443, name=f"{self.app.name}", targetPort=443, nodePort=30666)
-    self.service_patcher = KubernetesServicePatch(
-        self, [port], "LoadBalancer"
-    )
-    # ...
-```
-
-Port protocols can also be specified. Valid protocols are `"TCP"`, `"UDP"`, and `"SCTP"`
-
-```python
-# ...
-from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
-from lightkube.models.core_v1 import ServicePort
-
-class SomeCharm(CharmBase):
-  def __init__(self, *args):
-    # ...
-    tcp = ServicePort(443, name=f"{self.app.name}-tcp", protocol="TCP")
-    udp = ServicePort(443, name=f"{self.app.name}-udp", protocol="UDP")
-    sctp = ServicePort(443, name=f"{self.app.name}-sctp", protocol="SCTP")
-    self.service_patcher = KubernetesServicePatch(self, [tcp, udp, sctp])
-    # ...
-```
-
-Additionally, you may wish to use mocks in your charm's unit testing to ensure that the library
-does not try to make any API calls, or open any files during testing that are unlikely to be
-present, and could break your tests. The easiest way to do this is during your test `setUp`:
-
-```python
-# ...
-
-@patch("charm.KubernetesServicePatch", lambda x, y: None)
-def setUp(self, *unused):
-    self.harness = Harness(SomeCharm)
-    # ...
-```
-"""
-
-import logging
-from types import MethodType
-from typing import List, Literal
-
-from lightkube import ApiError, Client
-from lightkube.models.core_v1 import ServicePort, ServiceSpec
-from lightkube.models.meta_v1 import ObjectMeta
-from lightkube.resources.core_v1 import Service
-from lightkube.types import PatchType
-from ops.charm import CharmBase
-from ops.framework import Object
-
-logger = logging.getLogger(__name__)
-
-# The unique Charmhub library identifier, never change it
-LIBID = "0042f86d0a874435adef581806cddbbb"
-
-# 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 = 1
-
-ServiceType = Literal["ClusterIP", "LoadBalancer"]
-
-
-class KubernetesServicePatch(Object):
-    """A utility for patching the Kubernetes service set up by Juju."""
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        ports: List[ServicePort],
-        service_name: str = None,
-        service_type: ServiceType = "ClusterIP",
-        additional_labels: dict = None,
-        additional_selectors: dict = None,
-        additional_annotations: dict = None,
-    ):
-        """Constructor for KubernetesServicePatch.
-
-        Args:
-            charm: the charm that is instantiating the library.
-            ports: a list of ServicePorts
-            service_name: allows setting custom name to the patched service. If none given,
-                application name will be used.
-            service_type: desired type of K8s service. Default value is in line with ServiceSpec's
-                default value.
-            additional_labels: Labels to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_selectors: Selectors to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_annotations: Annotations to be added to the kubernetes service.
-        """
-        super().__init__(charm, "kubernetes-service-patch")
-        self.charm = charm
-        self.service_name = service_name if service_name else self._app
-        self.service = self._service_object(
-            ports,
-            service_name,
-            service_type,
-            additional_labels,
-            additional_selectors,
-            additional_annotations,
-        )
-
-        # Make mypy type checking happy that self._patch is a method
-        assert isinstance(self._patch, MethodType)
-        # Ensure this patch is applied during the 'install' and 'upgrade-charm' events
-        self.framework.observe(charm.on.install, self._patch)
-        self.framework.observe(charm.on.upgrade_charm, self._patch)
-
-    def _service_object(
-        self,
-        ports: List[ServicePort],
-        service_name: str = None,
-        service_type: ServiceType = "ClusterIP",
-        additional_labels: dict = None,
-        additional_selectors: dict = None,
-        additional_annotations: dict = None,
-    ) -> Service:
-        """Creates a valid Service representation.
-
-        Args:
-            ports: a list of ServicePorts
-            service_name: allows setting custom name to the patched service. If none given,
-                application name will be used.
-            service_type: desired type of K8s service. Default value is in line with ServiceSpec's
-                default value.
-            additional_labels: Labels to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_selectors: Selectors to be added to the kubernetes service (by default only
-                "app.kubernetes.io/name" is set to the service name)
-            additional_annotations: Annotations to be added to the kubernetes service.
-
-        Returns:
-            Service: A valid representation of a Kubernetes Service with the correct ports.
-        """
-        if not service_name:
-            service_name = self._app
-        labels = {"app.kubernetes.io/name": self._app}
-        if additional_labels:
-            labels.update(additional_labels)
-        selector = {"app.kubernetes.io/name": self._app}
-        if additional_selectors:
-            selector.update(additional_selectors)
-        return Service(
-            apiVersion="v1",
-            kind="Service",
-            metadata=ObjectMeta(
-                namespace=self._namespace,
-                name=service_name,
-                labels=labels,
-                annotations=additional_annotations,  # type: ignore[arg-type]
-            ),
-            spec=ServiceSpec(
-                selector=selector,
-                ports=ports,
-                type=service_type,
-            ),
-        )
-
-    def _patch(self, _) -> None:
-        """Patch the Kubernetes service created by Juju to map the correct port.
-
-        Raises:
-            PatchFailed: if patching fails due to lack of permissions, or otherwise.
-        """
-        if not self.charm.unit.is_leader():
-            return
-
-        client = Client()
-        try:
-            if self.service_name != self._app:
-                self._delete_and_create_service(client)
-            client.patch(Service, self.service_name, self.service, patch_type=PatchType.MERGE)
-        except ApiError as e:
-            if e.status.code == 403:
-                logger.error("Kubernetes service patch failed: `juju trust` this application.")
-            else:
-                logger.error("Kubernetes service patch failed: %s", str(e))
-        else:
-            logger.info("Kubernetes service '%s' patched successfully", self._app)
-
-    def _delete_and_create_service(self, client: Client):
-        service = client.get(Service, self._app, namespace=self._namespace)
-        service.metadata.name = self.service_name  # type: ignore[attr-defined]
-        service.metadata.resourceVersion = service.metadata.uid = None  # type: ignore[attr-defined]   # noqa: E501
-        client.delete(Service, self._app, namespace=self._namespace)
-        client.create(service)
-
-    def is_patched(self) -> bool:
-        """Reports if the service patch has been applied.
-
-        Returns:
-            bool: A boolean indicating if the service patch has been applied.
-        """
-        client = Client()
-        # Get the relevant service from the cluster
-        service = client.get(Service, name=self.service_name, namespace=self._namespace)
-        # Construct a list of expected ports, should the patch be applied
-        expected_ports = [(p.port, p.targetPort) for p in self.service.spec.ports]
-        # Construct a list in the same manner, using the fetched service
-        fetched_ports = [(p.port, p.targetPort) for p in service.spec.ports]  # type: ignore[attr-defined]  # noqa: E501
-        return expected_ports == fetched_ports
-
-    @property
-    def _app(self) -> str:
-        """Name of the current Juju application.
-
-        Returns:
-            str: A string containing the name of the current Juju application.
-        """
-        return self.charm.app.name
-
-    @property
-    def _namespace(self) -> str:
-        """The Kubernetes namespace we're running in.
-
-        Returns:
-            str: A string containing the name of the current Kubernetes namespace.
-        """
-        with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") as f:
-            return f.read().strip()
diff --git a/installers/charm/osm-ro/lib/charms/osm_libs/v0/utils.py b/installers/charm/osm-ro/lib/charms/osm_libs/v0/utils.py
deleted file mode 100644 (file)
index d739ba6..0000000
+++ /dev/null
@@ -1,544 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-#         http://www.apache.org/licenses/LICENSE-2.0
-"""OSM Utils Library.
-
-This library offers some utilities made for but not limited to Charmed OSM.
-
-# Getting started
-
-Execute the following command inside your Charmed Operator folder to fetch the library.
-
-```shell
-charmcraft fetch-lib charms.osm_libs.v0.utils
-```
-
-# CharmError Exception
-
-An exception that takes to arguments, the message and the StatusBase class, which are useful
-to set the status of the charm when the exception raises.
-
-Example:
-```shell
-from charms.osm_libs.v0.utils import CharmError
-
-class MyCharm(CharmBase):
-    def _on_config_changed(self, _):
-        try:
-            if not self.config.get("some-option"):
-                raise CharmError("need some-option", BlockedStatus)
-
-            if not self.mysql_ready:
-                raise CharmError("waiting for mysql", WaitingStatus)
-
-            # Do stuff...
-
-        exception CharmError as e:
-            self.unit.status = e.status
-```
-
-# Pebble validations
-
-The `check_container_ready` function checks that a container is ready,
-and therefore Pebble is ready.
-
-The `check_service_active` function checks that a service in a container is running.
-
-Both functions raise a CharmError if the validations fail.
-
-Example:
-```shell
-from charms.osm_libs.v0.utils import check_container_ready, check_service_active
-
-class MyCharm(CharmBase):
-    def _on_config_changed(self, _):
-        try:
-            container: Container = self.unit.get_container("my-container")
-            check_container_ready(container)
-            check_service_active(container, "my-service")
-            # Do stuff...
-
-        exception CharmError as e:
-            self.unit.status = e.status
-```
-
-# Debug-mode
-
-The debug-mode allows OSM developers to easily debug OSM modules.
-
-Example:
-```shell
-from charms.osm_libs.v0.utils import DebugMode
-
-class MyCharm(CharmBase):
-    _stored = StoredState()
-
-    def __init__(self, _):
-        # ...
-        container: Container = self.unit.get_container("my-container")
-        hostpaths = [
-            HostPath(
-                config="module-hostpath",
-                container_path="/usr/lib/python3/dist-packages/module"
-            ),
-        ]
-        vscode_workspace_path = "files/vscode-workspace.json"
-        self.debug_mode = DebugMode(
-            self,
-            self._stored,
-            container,
-            hostpaths,
-            vscode_workspace_path,
-        )
-
-    def _on_update_status(self, _):
-        if self.debug_mode.started:
-            return
-        # ...
-
-    def _get_debug_mode_information(self):
-        command = self.debug_mode.command
-        password = self.debug_mode.password
-        return command, password
-```
-
-# More
-
-- Get pod IP with `get_pod_ip()`
-"""
-from dataclasses import dataclass
-import logging
-import secrets
-import socket
-from pathlib import Path
-from typing import List
-
-from lightkube import Client
-from lightkube.models.core_v1 import HostPathVolumeSource, Volume, VolumeMount
-from lightkube.resources.apps_v1 import StatefulSet
-from ops.charm import CharmBase
-from ops.framework import Object, StoredState
-from ops.model import (
-    ActiveStatus,
-    BlockedStatus,
-    Container,
-    MaintenanceStatus,
-    StatusBase,
-    WaitingStatus,
-)
-from ops.pebble import ServiceStatus
-
-# The unique Charmhub library identifier, never change it
-LIBID = "e915908eebee4cdd972d484728adf984"
-
-# 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 = 5
-
-logger = logging.getLogger(__name__)
-
-
-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)
-
-
-def check_container_ready(container: Container) -> None:
-    """Check Pebble has started in the container.
-
-    Args:
-        container (Container): Container to be checked.
-
-    Raises:
-        CharmError: if container is not ready.
-    """
-    if not container.can_connect():
-        raise CharmError("waiting for pebble to start", MaintenanceStatus)
-
-
-def check_service_active(container: Container, service_name: str) -> None:
-    """Check if the service is running.
-
-    Args:
-        container (Container): Container to be checked.
-        service_name (str): Name of the service to check.
-
-    Raises:
-        CharmError: if the service is not running.
-    """
-    if service_name not in container.get_plan().services:
-        raise CharmError(f"{service_name} service not configured yet", WaitingStatus)
-
-    if container.get_service(service_name).current != ServiceStatus.ACTIVE:
-        raise CharmError(f"{service_name} service is not running")
-
-
-def get_pod_ip() -> str:
-    """Get Kubernetes Pod IP.
-
-    Returns:
-        str: The IP of the Pod.
-    """
-    return socket.gethostbyname(socket.gethostname())
-
-
-_DEBUG_SCRIPT = r"""#!/bin/bash
-# Install SSH
-
-function download_code(){{
-    wget https://go.microsoft.com/fwlink/?LinkID=760868 -O code.deb
-}}
-
-function setup_envs(){{
-    grep "source /debug.envs" /root/.bashrc || echo "source /debug.envs" | tee -a /root/.bashrc
-}}
-function setup_ssh(){{
-    apt install ssh -y
-    cat /etc/ssh/sshd_config |
-        grep -E '^PermitRootLogin yes$$' || (
-        echo PermitRootLogin yes |
-        tee -a /etc/ssh/sshd_config
-    )
-    service ssh stop
-    sleep 3
-    service ssh start
-    usermod --password $(echo {} | openssl passwd -1 -stdin) root
-}}
-
-function setup_code(){{
-    apt install libasound2 -y
-    (dpkg -i code.deb || apt-get install -f -y || apt-get install -f -y) && echo Code installed successfully
-    code --install-extension ms-python.python --user-data-dir /root
-    mkdir -p /root/.vscode-server
-    cp -R /root/.vscode/extensions /root/.vscode-server/extensions
-}}
-
-export DEBIAN_FRONTEND=noninteractive
-apt update && apt install wget -y
-download_code &
-setup_ssh &
-setup_envs
-wait
-setup_code &
-wait
-"""
-
-
-@dataclass
-class SubModule:
-    """Represent RO Submodules."""
-    sub_module_path: str
-    container_path: str
-
-
-class HostPath:
-    """Represents a hostpath."""
-    def __init__(self, config: str, container_path: str, submodules: dict = None) -> None:
-        mount_path_items = config.split("-")
-        mount_path_items.reverse()
-        self.mount_path = "/" + "/".join(mount_path_items)
-        self.config = config
-        self.sub_module_dict = {}
-        if submodules:
-            for submodule in submodules.keys():
-                self.sub_module_dict[submodule] = SubModule(
-                    sub_module_path=self.mount_path + "/" + submodule + "/" + submodules[submodule].split("/")[-1],
-                    container_path=submodules[submodule],
-                )
-        else:
-            self.container_path = container_path
-            self.module_name = container_path.split("/")[-1]
-
-class DebugMode(Object):
-    """Class to handle the debug-mode."""
-
-    def __init__(
-        self,
-        charm: CharmBase,
-        stored: StoredState,
-        container: Container,
-        hostpaths: List[HostPath] = [],
-        vscode_workspace_path: str = "files/vscode-workspace.json",
-    ) -> None:
-        super().__init__(charm, "debug-mode")
-
-        self.charm = charm
-        self._stored = stored
-        self.hostpaths = hostpaths
-        self.vscode_workspace = Path(vscode_workspace_path).read_text()
-        self.container = container
-
-        self._stored.set_default(
-            debug_mode_started=False,
-            debug_mode_vscode_command=None,
-            debug_mode_password=None,
-        )
-
-        self.framework.observe(self.charm.on.config_changed, self._on_config_changed)
-        self.framework.observe(self.charm.on[container.name].pebble_ready, self._on_config_changed)
-        self.framework.observe(self.charm.on.update_status, self._on_update_status)
-
-    def _on_config_changed(self, _) -> None:
-        """Handler for the config-changed event."""
-        if not self.charm.unit.is_leader():
-            return
-
-        debug_mode_enabled = self.charm.config.get("debug-mode", False)
-        action = self.enable if debug_mode_enabled else self.disable
-        action()
-
-    def _on_update_status(self, _) -> None:
-        """Handler for the update-status event."""
-        if not self.charm.unit.is_leader() or not self.started:
-            return
-
-        self.charm.unit.status = ActiveStatus("debug-mode: ready")
-
-    @property
-    def started(self) -> bool:
-        """Indicates whether the debug-mode has started or not."""
-        return self._stored.debug_mode_started
-
-    @property
-    def command(self) -> str:
-        """Command to launch vscode."""
-        return self._stored.debug_mode_vscode_command
-
-    @property
-    def password(self) -> str:
-        """SSH password."""
-        return self._stored.debug_mode_password
-
-    def enable(self, service_name: str = None) -> None:
-        """Enable debug-mode.
-
-        This function mounts hostpaths of the OSM modules (if set), and
-        configures the container so it can be easily debugged. The setup
-        includes the configuration of SSH, environment variables, and
-        VSCode workspace and plugins.
-
-        Args:
-            service_name (str, optional): Pebble service name which has the desired environment
-                variables. Mandatory if there is more than one Pebble service configured.
-        """
-        hostpaths_to_reconfigure = self._hostpaths_to_reconfigure()
-        if self.started and not hostpaths_to_reconfigure:
-            self.charm.unit.status = ActiveStatus("debug-mode: ready")
-            return
-
-        logger.debug("enabling debug-mode")
-
-        # Mount hostpaths if set.
-        # If hostpaths are mounted, the statefulset will be restarted,
-        # and for that reason we return immediately. On restart, the hostpaths
-        # won't be mounted and then we can continue and setup the debug-mode.
-        if hostpaths_to_reconfigure:
-            self.charm.unit.status = MaintenanceStatus("debug-mode: configuring hostpaths")
-            self._configure_hostpaths(hostpaths_to_reconfigure)
-            return
-
-        self.charm.unit.status = MaintenanceStatus("debug-mode: starting")
-        password = secrets.token_hex(8)
-        self._setup_debug_mode(
-            password,
-            service_name,
-            mounted_hostpaths=[hp for hp in self.hostpaths if self.charm.config.get(hp.config)],
-        )
-
-        self._stored.debug_mode_vscode_command = self._get_vscode_command(get_pod_ip())
-        self._stored.debug_mode_password = password
-        self._stored.debug_mode_started = True
-        logger.info("debug-mode is ready")
-        self.charm.unit.status = ActiveStatus("debug-mode: ready")
-
-    def disable(self) -> None:
-        """Disable debug-mode."""
-        logger.debug("disabling debug-mode")
-        current_status = self.charm.unit.status
-        hostpaths_unmounted = self._unmount_hostpaths()
-
-        if not self._stored.debug_mode_started:
-            return
-        self._stored.debug_mode_started = False
-        self._stored.debug_mode_vscode_command = None
-        self._stored.debug_mode_password = None
-
-        if not hostpaths_unmounted:
-            self.charm.unit.status = current_status
-            self._restart()
-
-    def _hostpaths_to_reconfigure(self) -> List[HostPath]:
-        hostpaths_to_reconfigure: List[HostPath] = []
-        client = Client()
-        statefulset = client.get(StatefulSet, self.charm.app.name, namespace=self.charm.model.name)
-        volumes = statefulset.spec.template.spec.volumes
-
-        for hostpath in self.hostpaths:
-            hostpath_is_set = True if self.charm.config.get(hostpath.config) else False
-            hostpath_already_configured = next(
-                (True for volume in volumes if volume.name == hostpath.config), False
-            )
-            if hostpath_is_set != hostpath_already_configured:
-                hostpaths_to_reconfigure.append(hostpath)
-
-        return hostpaths_to_reconfigure
-
-    def _setup_debug_mode(
-        self,
-        password: str,
-        service_name: str = None,
-        mounted_hostpaths: List[HostPath] = [],
-    ) -> None:
-        services = self.container.get_plan().services
-        if not service_name and len(services) != 1:
-            raise Exception("Cannot start debug-mode: please set the service_name")
-
-        service = None
-        if not service_name:
-            service_name, service = services.popitem()
-        if not service:
-            service = services.get(service_name)
-
-        logger.debug(f"getting environment variables from service {service_name}")
-        environment = service.environment
-        environment_file_content = "\n".join(
-            [f'export {key}="{value}"' for key, value in environment.items()]
-        )
-        logger.debug(f"pushing environment file to {self.container.name} container")
-        self.container.push("/debug.envs", environment_file_content)
-
-        # Push VSCode workspace
-        logger.debug(f"pushing vscode workspace to {self.container.name} container")
-        self.container.push("/debug.code-workspace", self.vscode_workspace)
-
-        # Execute debugging script
-        logger.debug(f"pushing debug-mode setup script to {self.container.name} container")
-        self.container.push("/debug.sh", _DEBUG_SCRIPT.format(password), permissions=0o777)
-        logger.debug(f"executing debug-mode setup script in {self.container.name} container")
-        self.container.exec(["/debug.sh"]).wait_output()
-        logger.debug(f"stopping service {service_name} in {self.container.name} container")
-        self.container.stop(service_name)
-
-        # Add symlinks to mounted hostpaths
-        for hostpath in mounted_hostpaths:
-            logger.debug(f"adding symlink for {hostpath.config}")
-            if len(hostpath.sub_module_dict) > 0:
-                for sub_module in hostpath.sub_module_dict.keys():
-                    self.container.exec(["rm", "-rf", hostpath.sub_module_dict[sub_module].container_path]).wait_output()
-                    self.container.exec(
-                        [
-                            "ln",
-                            "-s",
-                            hostpath.sub_module_dict[sub_module].sub_module_path,
-                            hostpath.sub_module_dict[sub_module].container_path,
-                        ]
-                    )
-
-            else:
-                self.container.exec(["rm", "-rf", hostpath.container_path]).wait_output()
-                self.container.exec(
-                    [
-                        "ln",
-                        "-s",
-                        f"{hostpath.mount_path}/{hostpath.module_name}",
-                        hostpath.container_path,
-                    ]
-                )
-
-    def _configure_hostpaths(self, hostpaths: List[HostPath]):
-        client = Client()
-        statefulset = client.get(StatefulSet, self.charm.app.name, namespace=self.charm.model.name)
-
-        for hostpath in hostpaths:
-            if self.charm.config.get(hostpath.config):
-                self._add_hostpath_to_statefulset(hostpath, statefulset)
-            else:
-                self._delete_hostpath_from_statefulset(hostpath, statefulset)
-
-        client.replace(statefulset)
-
-    def _unmount_hostpaths(self) -> bool:
-        client = Client()
-        hostpath_unmounted = False
-        statefulset = client.get(StatefulSet, self.charm.app.name, namespace=self.charm.model.name)
-
-        for hostpath in self.hostpaths:
-            if self._delete_hostpath_from_statefulset(hostpath, statefulset):
-                hostpath_unmounted = True
-
-        if hostpath_unmounted:
-            client.replace(statefulset)
-
-        return hostpath_unmounted
-
-    def _add_hostpath_to_statefulset(self, hostpath: HostPath, statefulset: StatefulSet):
-        # Add volume
-        logger.debug(f"adding volume {hostpath.config} to {self.charm.app.name} statefulset")
-        volume = Volume(
-            hostpath.config,
-            hostPath=HostPathVolumeSource(
-                path=self.charm.config[hostpath.config],
-                type="Directory",
-            ),
-        )
-        statefulset.spec.template.spec.volumes.append(volume)
-
-        # Add volumeMount
-        for statefulset_container in statefulset.spec.template.spec.containers:
-            if statefulset_container.name != self.container.name:
-                continue
-
-            logger.debug(
-                f"adding volumeMount {hostpath.config} to {self.container.name} container"
-            )
-            statefulset_container.volumeMounts.append(
-                VolumeMount(mountPath=hostpath.mount_path, name=hostpath.config)
-            )
-
-    def _delete_hostpath_from_statefulset(self, hostpath: HostPath, statefulset: StatefulSet):
-        hostpath_unmounted = False
-        for volume in statefulset.spec.template.spec.volumes:
-
-            if hostpath.config != volume.name:
-                continue
-
-            # Remove volumeMount
-            for statefulset_container in statefulset.spec.template.spec.containers:
-                if statefulset_container.name != self.container.name:
-                    continue
-                for volume_mount in statefulset_container.volumeMounts:
-                    if volume_mount.name != hostpath.config:
-                        continue
-
-                    logger.debug(
-                        f"removing volumeMount {hostpath.config} from {self.container.name} container"
-                    )
-                    statefulset_container.volumeMounts.remove(volume_mount)
-
-            # Remove volume
-            logger.debug(
-                f"removing volume {hostpath.config} from {self.charm.app.name} statefulset"
-            )
-            statefulset.spec.template.spec.volumes.remove(volume)
-
-            hostpath_unmounted = True
-        return hostpath_unmounted
-
-    def _get_vscode_command(
-        self,
-        pod_ip: str,
-        user: str = "root",
-        workspace_path: str = "/debug.code-workspace",
-    ) -> str:
-        return f"code --remote ssh-remote+{user}@{pod_ip} {workspace_path}"
-
-    def _restart(self):
-        self.container.exec(["kill", "-HUP", "1"])
diff --git a/installers/charm/osm-ro/lib/charms/osm_ro/v0/ro.py b/installers/charm/osm-ro/lib/charms/osm_ro/v0/ro.py
deleted file mode 100644 (file)
index 79bee5e..0000000
+++ /dev/null
@@ -1,178 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# Learn more at: https://juju.is/docs/sdk
-
-"""Ro library.
-
-This [library](https://juju.is/docs/sdk/libraries) implements both sides of the
-`ro` [interface](https://juju.is/docs/sdk/relations).
-
-The *provider* side of this interface is implemented by the
-[osm-ro Charmed Operator](https://charmhub.io/osm-ro).
-
-Any Charmed Operator that *requires* RO for providing its
-service should implement the *requirer* side of this interface.
-
-In a nutshell using this library to implement a Charmed Operator *requiring*
-RO would look like
-
-```
-$ charmcraft fetch-lib charms.osm_ro.v0.ro
-```
-
-`metadata.yaml`:
-
-```
-requires:
-  ro:
-    interface: ro
-    limit: 1
-```
-
-`src/charm.py`:
-
-```
-from charms.osm_ro.v0.ro import RoRequires
-from ops.charm import CharmBase
-
-
-class MyCharm(CharmBase):
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self.ro = RoRequires(self)
-        self.framework.observe(
-            self.on["ro"].relation_changed,
-            self._on_ro_relation_changed,
-        )
-        self.framework.observe(
-            self.on["ro"].relation_broken,
-            self._on_ro_relation_broken,
-        )
-        self.framework.observe(
-            self.on["ro"].relation_broken,
-            self._on_ro_broken,
-        )
-
-    def _on_ro_relation_broken(self, event):
-        # Get RO host and port
-        host: str = self.ro.host
-        port: int = self.ro.port
-        # host => "osm-ro"
-        # port => 9999
-
-    def _on_ro_broken(self, event):
-        # Stop service
-        # ...
-        self.unit.status = BlockedStatus("need ro relation")
-```
-
-You can file bugs
-[here](https://osm.etsi.org/bugzilla/enter_bug.cgi), selecting the `devops` module!
-"""
-from typing import Optional
-
-from ops.charm import CharmBase, CharmEvents
-from ops.framework import EventBase, EventSource, Object
-from ops.model import Relation
-
-
-# The unique Charmhub library identifier, never change it
-LIBID = "a34c3331a43f4f6db2b1499ff4d1390d"
-
-# 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 = 1
-
-RO_HOST_APP_KEY = "host"
-RO_PORT_APP_KEY = "port"
-
-
-class RoRequires(Object):  # pragma: no cover
-    """Requires-side of the Ro relation."""
-
-    def __init__(self, charm: CharmBase, endpoint_name: str = "ro") -> None:
-        super().__init__(charm, endpoint_name)
-        self.charm = charm
-        self._endpoint_name = endpoint_name
-
-    @property
-    def host(self) -> str:
-        """Get ro hostname."""
-        relation: Relation = self.model.get_relation(self._endpoint_name)
-        return (
-            relation.data[relation.app].get(RO_HOST_APP_KEY)
-            if relation and relation.app
-            else None
-        )
-
-    @property
-    def port(self) -> int:
-        """Get ro port number."""
-        relation: Relation = self.model.get_relation(self._endpoint_name)
-        return (
-            int(relation.data[relation.app].get(RO_PORT_APP_KEY))
-            if relation and relation.app
-            else None
-        )
-
-
-class RoProvides(Object):
-    """Provides-side of the Ro relation."""
-
-    def __init__(self, charm: CharmBase, endpoint_name: str = "ro") -> None:
-        super().__init__(charm, endpoint_name)
-        self._endpoint_name = endpoint_name
-
-    def set_host_info(self, host: str, port: int, relation: Optional[Relation] = None) -> None:
-        """Set Ro host and port.
-
-        This function writes in the application data of the relation, therefore,
-        only the unit leader can call it.
-
-        Args:
-            host (str): Ro hostname or IP address.
-            port (int): Ro port.
-            relation (Optional[Relation]): Relation to update.
-                                           If not specified, all relations will be updated.
-
-        Raises:
-            Exception: if a non-leader unit calls this function.
-        """
-        if not self.model.unit.is_leader():
-            raise Exception("only the leader set host information.")
-
-        if relation:
-            self._update_relation_data(host, port, relation)
-            return
-
-        for relation in self.model.relations[self._endpoint_name]:
-            self._update_relation_data(host, port, relation)
-
-    def _update_relation_data(self, host: str, port: int, relation: Relation) -> None:
-        """Update data in relation if needed."""
-        relation.data[self.model.app][RO_HOST_APP_KEY] = host
-        relation.data[self.model.app][RO_PORT_APP_KEY] = str(port)
diff --git a/installers/charm/osm-ro/metadata.yaml b/installers/charm/osm-ro/metadata.yaml
deleted file mode 100644 (file)
index a94036a..0000000
+++ /dev/null
@@ -1,66 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# This file populates the Overview on Charmhub.
-# See https://juju.is/docs/some-url-to-be-determined/ for a checklist and guidance.
-
-name: osm-ro
-
-# The following metadata are human-readable and will be published prominently on Charmhub.
-
-display-name: OSM RO
-
-summary: OSM Resource Orchestrator (RO)
-
-description: |
-  A Kubernetes operator that deploys the Resource Orchestrator of OSM.
-
-  Resource orchestrator module's main responsibility is managing the
-  VIM and SDN operations by taking orders through the LCM and Kafka
-  message queue.
-
-  This charm doesn't make sense on its own.
-  See more:
-    - https://charmhub.io/osm
-
-containers:
-  ro:
-    resource: ro-image
-
-# This file populates the Resources tab on Charmhub.
-
-resources:
-  ro-image:
-    type: oci-image
-    description: OCI image for ro
-    upstream-source: opensourcemano/ro
-
-requires:
-  kafka:
-    interface: kafka
-    limit: 1
-  mongodb:
-    interface: mongodb_client
-    limit: 1
-
-provides:
-  ro:
-    interface: ro
diff --git a/installers/charm/osm-ro/pyproject.toml b/installers/charm/osm-ro/pyproject.toml
deleted file mode 100644 (file)
index 16cf0f4..0000000
+++ /dev/null
@@ -1,52 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-
-# 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", "E501", "D107"]
-# D100, D101, D102, D103: Ignore missing docstrings in tests
-per-file-ignores = ["tests/*:D100,D101,D102,D103,D104"]
-docstring-convention = "google"
diff --git a/installers/charm/osm-ro/requirements.txt b/installers/charm/osm-ro/requirements.txt
deleted file mode 100644 (file)
index 398d4ad..0000000
+++ /dev/null
@@ -1,23 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-ops < 2.2
-lightkube
-lightkube-models
-# git+https://github.com/charmed-osm/config-validator/
diff --git a/installers/charm/osm-ro/src/charm.py b/installers/charm/osm-ro/src/charm.py
deleted file mode 100755 (executable)
index 89da4f1..0000000
+++ /dev/null
@@ -1,338 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-#
-# Learn more at: https://juju.is/docs/sdk
-
-"""OSM RO charm.
-
-See more: https://charmhub.io/osm
-"""
-
-import base64
-import logging
-from typing import Any, Dict
-
-from charms.data_platform_libs.v0.data_interfaces import DatabaseRequires
-from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
-from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
-from charms.osm_libs.v0.utils import (
-    CharmError,
-    DebugMode,
-    HostPath,
-    check_container_ready,
-    check_service_active,
-)
-from charms.osm_ro.v0.ro import RoProvides
-from lightkube.models.core_v1 import ServicePort
-from ops.charm import ActionEvent, CharmBase, RelationJoinedEvent
-from ops.framework import StoredState
-from ops.main import main
-from ops.model import ActiveStatus, Container
-
-ro_host_paths = {
-    "NG-RO": "/usr/lib/python3/dist-packages/osm_ng_ro",
-    "RO-plugin": "/usr/lib/python3/dist-packages/osm_ro_plugin",
-    "RO-SDN-arista_cloudvision": "/usr/lib/python3/dist-packages/osm_rosdn_arista_cloudvision",
-    "RO-SDN-dpb": "/usr/lib/python3/dist-packages/osm_rosdn_dpb",
-    "RO-SDN-dynpac": "/usr/lib/python3/dist-packages/osm_rosdn_dynpac",
-    "RO-SDN-floodlight_openflow": "/usr/lib/python3/dist-packages/osm_rosdn_floodlightof",
-    "RO-SDN-ietfl2vpn": "/usr/lib/python3/dist-packages/osm_rosdn_ietfl2vpn",
-    "RO-SDN-juniper_contrail": "/usr/lib/python3/dist-packages/osm_rosdn_juniper_contrail",
-    "RO-SDN-odl_openflow": "/usr/lib/python3/dist-packages/osm_rosdn_odlof",
-    "RO-SDN-onos_openflow": "/usr/lib/python3/dist-packages/osm_rosdn_onosof",
-    "RO-SDN-onos_vpls": "/usr/lib/python3/dist-packages/osm_rosdn_onos_vpls",
-    "RO-VIM-aws": "/usr/lib/python3/dist-packages/osm_rovim_aws",
-    "RO-VIM-azure": "/usr/lib/python3/dist-packages/osm_rovim_azure",
-    "RO-VIM-gcp": "/usr/lib/python3/dist-packages/osm_rovim_gcp",
-    "RO-VIM-openstack": "/usr/lib/python3/dist-packages/osm_rovim_openstack",
-    "RO-VIM-openvim": "/usr/lib/python3/dist-packages/osm_rovim_openvim",
-    "RO-VIM-vmware": "/usr/lib/python3/dist-packages/osm_rovim_vmware",
-}
-HOSTPATHS = [
-    HostPath(
-        config="ro-hostpath",
-        container_path="/usr/lib/python3/dist-packages/",
-        submodules=ro_host_paths,
-    ),
-    HostPath(
-        config="common-hostpath",
-        container_path="/usr/lib/python3/dist-packages/osm_common",
-    ),
-]
-SERVICE_PORT = 9090
-USER = GROUP = "appuser"
-
-logger = logging.getLogger(__name__)
-
-
-def decode(content: str):
-    """Base64 decoding of a string."""
-    return base64.b64decode(content.encode("utf-8")).decode("utf-8")
-
-
-class OsmRoCharm(CharmBase):
-    """OSM RO Kubernetes sidecar charm."""
-
-    on = KafkaEvents()
-    service_name = "ro"
-    _stored = StoredState()
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self._stored.set_default(certificates=set())
-        self.kafka = KafkaRequires(self)
-        self.mongodb_client = DatabaseRequires(self, "mongodb", database_name="osm")
-        self._observe_charm_events()
-        self._patch_k8s_service()
-        self.ro = RoProvides(self)
-        self.container: Container = self.unit.get_container("ro")
-        self.debug_mode = DebugMode(self, self._stored, self.container, HOSTPATHS)
-
-    # ---------------------------------------------------------------------------
-    #   Handlers for Charm Events
-    # ---------------------------------------------------------------------------
-
-    def _on_config_changed(self, _) -> None:
-        """Handler for the config-changed event."""
-        try:
-            self._validate_config()
-            self._check_relations()
-            # Check if the container is ready.
-            # Eventually it will become ready after the first pebble-ready event.
-            check_container_ready(self.container)
-
-            self._configure_certificates()
-            if not self.debug_mode.started:
-                self._configure_service()
-            self._update_ro_relation()
-
-            # Update charm status
-            self._on_update_status()
-        except CharmError as e:
-            logger.debug(e.message)
-            self.unit.status = e.status
-
-    def _on_update_status(self, _=None) -> None:
-        """Handler for the update-status event."""
-        try:
-            self._validate_config()
-            self._check_relations()
-            check_container_ready(self.container)
-            if self.debug_mode.started:
-                return
-            check_service_active(self.container, self.service_name)
-            self.unit.status = ActiveStatus()
-        except CharmError as e:
-            logger.debug(e.message)
-            self.unit.status = e.status
-
-    def _on_required_relation_broken(self, _) -> None:
-        """Handler for the kafka-broken event."""
-        try:
-            check_container_ready(self.container)
-            check_service_active(self.container, "ro")
-            self.container.stop("ro")
-        except CharmError:
-            pass
-
-        self._on_update_status()
-
-    def _update_ro_relation(self, event: RelationJoinedEvent = None) -> None:
-        """Handler for the ro-relation-joined event."""
-        try:
-            if self.unit.is_leader():
-                check_container_ready(self.container)
-                check_service_active(self.container, "ro")
-                self.ro.set_host_info(
-                    self.app.name, SERVICE_PORT, event.relation if event else None
-                )
-        except CharmError as e:
-            self.unit.status = e.status
-
-    def _on_get_debug_mode_information_action(self, event: ActionEvent) -> None:
-        """Handler for the get-debug-mode-information action event."""
-        if not self.debug_mode.started:
-            event.fail(
-                f"debug-mode has not started. Hint: juju config {self.app.name} debug-mode=true"
-            )
-            return
-
-        debug_info = {"command": self.debug_mode.command, "password": self.debug_mode.password}
-        event.set_results(debug_info)
-
-    # ---------------------------------------------------------------------------
-    #   Validation and configuration and more
-    # ---------------------------------------------------------------------------
-
-    def _patch_k8s_service(self) -> None:
-        port = ServicePort(SERVICE_PORT, name=f"{self.app.name}")
-        self.service_patcher = KubernetesServicePatch(self, [port])
-
-    def _observe_charm_events(self) -> None:
-        event_handler_mapping = {
-            # Core lifecycle events
-            self.on.ro_pebble_ready: self._on_config_changed,
-            self.on.config_changed: self._on_config_changed,
-            self.on.update_status: self._on_update_status,
-            # Relation events
-            self.on.kafka_available: self._on_config_changed,
-            self.on["kafka"].relation_broken: self._on_required_relation_broken,
-            self.mongodb_client.on.database_created: self._on_config_changed,
-            self.on["mongodb"].relation_broken: self._on_required_relation_broken,
-            self.on.ro_relation_joined: self._update_ro_relation,
-            # Action events
-            self.on.get_debug_mode_information_action: self._on_get_debug_mode_information_action,
-        }
-
-        for event, handler in event_handler_mapping.items():
-            self.framework.observe(event, handler)
-
-    def _is_database_available(self) -> bool:
-        try:
-            return self.mongodb_client.is_resource_created()
-        except KeyError:
-            return False
-
-    def _validate_config(self) -> None:
-        """Validate charm configuration.
-
-        Raises:
-            CharmError: if charm configuration is invalid.
-        """
-        logger.debug("validating charm config")
-        if self.config["log-level"].upper() not in [
-            "TRACE",
-            "DEBUG",
-            "INFO",
-            "WARN",
-            "ERROR",
-            "FATAL",
-        ]:
-            raise CharmError("invalid value for log-level option")
-
-        refresh_period = self.config.get("period_refresh_active")
-        if refresh_period and refresh_period < 60 and refresh_period != -1:
-            raise ValueError(
-                "Refresh Period is too tight, insert >= 60 seconds or disable using -1"
-            )
-
-    def _check_relations(self) -> None:
-        """Validate charm relations.
-
-        Raises:
-            CharmError: if charm configuration is invalid.
-        """
-        logger.debug("check for missing relations")
-        missing_relations = []
-
-        if not self.kafka.host or not self.kafka.port:
-            missing_relations.append("kafka")
-        if not self._is_database_available():
-            missing_relations.append("mongodb")
-
-        if missing_relations:
-            relations_str = ", ".join(missing_relations)
-            one_relation_missing = len(missing_relations) == 1
-            error_msg = f'need {relations_str} relation{"" if one_relation_missing else "s"}'
-            logger.warning(error_msg)
-            raise CharmError(error_msg)
-
-    def _configure_certificates(self) -> None:
-        """Push certificates to the RO container."""
-        if not (certificate_config := self.config.get("certificates")):
-            return
-
-        certificates_list = certificate_config.split(",")
-        updated_certificates = set()
-
-        for certificate in certificates_list:
-            if ":" not in certificate:
-                continue
-            name, content = certificate.split(":")
-            content = decode(content)
-            self.container.push(
-                f"/certs/{name}",
-                content,
-                permissions=0o400,
-                make_dirs=True,
-                user=USER,
-                group=GROUP,
-            )
-            updated_certificates.add(name)
-            self._stored.certificates.add(name)
-            logger.info(f"certificate {name} pushed successfully")
-
-        stored_certificates = {c for c in self._stored.certificates}
-        for certificate_to_remove in stored_certificates.difference(updated_certificates):
-            self.container.remove_path(f"/certs/{certificate_to_remove}")
-            self._stored.certificates.remove(certificate_to_remove)
-            logger.info(f"certificate {certificate_to_remove} removed successfully")
-
-    def _configure_service(self) -> None:
-        """Add Pebble layer with the ro service."""
-        logger.debug(f"configuring {self.app.name} service")
-        self.container.add_layer("ro", self._get_layer(), combine=True)
-        self.container.replan()
-
-    def _get_layer(self) -> Dict[str, Any]:
-        """Get layer for Pebble."""
-        return {
-            "summary": "ro layer",
-            "description": "pebble config layer for ro",
-            "services": {
-                "ro": {
-                    "override": "replace",
-                    "summary": "ro service",
-                    "command": "/bin/sh -c 'cd /app/osm_ro && python3 -u -m osm_ng_ro.ro_main'",  # cd /app/osm_nbi is needed until we upgrade Juju to 3.x.
-                    "startup": "enabled",
-                    "user": USER,
-                    "group": GROUP,
-                    "working-dir": "/app/osm_ro",  # This parameter has no effect in Juju 2.9.x.
-                    "environment": {
-                        # General configuration
-                        "OSMRO_LOG_LEVEL": self.config["log-level"].upper(),
-                        # Kafka configuration
-                        "OSMRO_MESSAGE_HOST": self.kafka.host,
-                        "OSMRO_MESSAGE_PORT": self.kafka.port,
-                        "OSMRO_MESSAGE_DRIVER": "kafka",
-                        # Database configuration
-                        "OSMRO_DATABASE_DRIVER": "mongo",
-                        "OSMRO_DATABASE_URI": self._get_mongodb_uri(),
-                        "OSMRO_DATABASE_COMMONKEY": self.config["database-commonkey"],
-                        # Storage configuration
-                        "OSMRO_STORAGE_DRIVER": "mongo",
-                        "OSMRO_STORAGE_PATH": "/app/storage",
-                        "OSMRO_STORAGE_COLLECTION": "files",
-                        "OSMRO_STORAGE_URI": self._get_mongodb_uri(),
-                        "OSMRO_PERIOD_REFRESH_ACTIVE": self.config.get("period_refresh_active")
-                        or 60,
-                    },
-                }
-            },
-        }
-
-    def _get_mongodb_uri(self):
-        return list(self.mongodb_client.fetch_relation_data().values())[0]["uris"]
-
-
-if __name__ == "__main__":  # pragma: no cover
-    main(OsmRoCharm)
diff --git a/installers/charm/osm-ro/src/legacy_interfaces.py b/installers/charm/osm-ro/src/legacy_interfaces.py
deleted file mode 100644 (file)
index da9483e..0000000
+++ /dev/null
@@ -1,110 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-# flake8: noqa
-
-import ops
-
-
-class BaseRelationClient(ops.framework.Object):
-    """Requires side of a Kafka Endpoint"""
-
-    def __init__(
-        self,
-        charm: ops.charm.CharmBase,
-        relation_name: str,
-        mandatory_fields: list = [],
-    ):
-        super().__init__(charm, relation_name)
-        self.relation_name = relation_name
-        self.mandatory_fields = mandatory_fields
-        self._update_relation()
-
-    def get_data_from_unit(self, key: str):
-        if not self.relation:
-            # This update relation doesn't seem to be needed, but I added it because apparently
-            # the data is empty in the unit tests.
-            # In reality, the constructor is called in every hook.
-            # In the unit tests when doing an update_relation_data, apparently it is not called.
-            self._update_relation()
-        if self.relation:
-            for unit in self.relation.units:
-                data = self.relation.data[unit].get(key)
-                if data:
-                    return data
-
-    def get_data_from_app(self, key: str):
-        if not self.relation or self.relation.app not in self.relation.data:
-            # This update relation doesn't seem to be needed, but I added it because apparently
-            # the data is empty in the unit tests.
-            # In reality, the constructor is called in every hook.
-            # In the unit tests when doing an update_relation_data, apparently it is not called.
-            self._update_relation()
-        if self.relation and self.relation.app in self.relation.data:
-            data = self.relation.data[self.relation.app].get(key)
-            if data:
-                return data
-
-    def is_missing_data_in_unit(self):
-        return not all([self.get_data_from_unit(field) for field in self.mandatory_fields])
-
-    def is_missing_data_in_app(self):
-        return not all([self.get_data_from_app(field) for field in self.mandatory_fields])
-
-    def _update_relation(self):
-        self.relation = self.framework.model.get_relation(self.relation_name)
-
-
-class MongoClient(BaseRelationClient):
-    """Requires side of a Mongo Endpoint"""
-
-    mandatory_fields_mapping = {
-        "reactive": ["connection_string"],
-        "ops": ["replica_set_uri", "replica_set_name"],
-    }
-
-    def __init__(self, charm: ops.charm.CharmBase, relation_name: str):
-        super().__init__(charm, relation_name, mandatory_fields=[])
-
-    @property
-    def connection_string(self):
-        if self.is_opts():
-            replica_set_uri = self.get_data_from_unit("replica_set_uri")
-            replica_set_name = self.get_data_from_unit("replica_set_name")
-            return f"{replica_set_uri}?replicaSet={replica_set_name}"
-        else:
-            return self.get_data_from_unit("connection_string")
-
-    def is_opts(self):
-        return not self.is_missing_data_in_unit_ops()
-
-    def is_missing_data_in_unit(self):
-        return self.is_missing_data_in_unit_ops() and self.is_missing_data_in_unit_reactive()
-
-    def is_missing_data_in_unit_ops(self):
-        return not all(
-            [self.get_data_from_unit(field) for field in self.mandatory_fields_mapping["ops"]]
-        )
-
-    def is_missing_data_in_unit_reactive(self):
-        return not all(
-            [self.get_data_from_unit(field) for field in self.mandatory_fields_mapping["reactive"]]
-        )
diff --git a/installers/charm/osm-ro/tests/integration/test_charm.py b/installers/charm/osm-ro/tests/integration/test_charm.py
deleted file mode 100644 (file)
index 38e9ad9..0000000
+++ /dev/null
@@ -1,100 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-# Learn more about testing at: https://juju.is/docs/sdk/testing
-
-import asyncio
-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())
-RO_APP = METADATA["name"]
-KAFKA_CHARM = "kafka-k8s"
-KAFKA_APP = "kafka"
-MONGO_DB_CHARM = "mongodb-k8s"
-MONGO_DB_APP = "mongodb"
-ZOOKEEPER_CHARM = "zookeeper-k8s"
-ZOOKEEPER_APP = "zookeeper"
-APPS = [KAFKA_APP, MONGO_DB_APP, ZOOKEEPER_APP, RO_APP]
-
-
-@pytest.mark.abort_on_fail
-async def test_ro_is_deployed(ops_test: OpsTest):
-    charm = await ops_test.build_charm(".")
-    resources = {"ro-image": METADATA["resources"]["ro-image"]["upstream-source"]}
-
-    await asyncio.gather(
-        ops_test.model.deploy(charm, resources=resources, application_name=RO_APP, series="jammy"),
-        ops_test.model.deploy(ZOOKEEPER_CHARM, application_name=ZOOKEEPER_APP, channel="stable"),
-        ops_test.model.deploy(KAFKA_CHARM, application_name=KAFKA_APP, channel="stable"),
-        ops_test.model.deploy(MONGO_DB_CHARM, application_name=MONGO_DB_APP, channel="5/edge"),
-    )
-
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS,
-            timeout=300,
-        )
-    assert ops_test.model.applications[RO_APP].status == "blocked"
-    unit = ops_test.model.applications[RO_APP].units[0]
-    assert unit.workload_status_message == "need kafka, mongodb relations"
-
-    logger.info("Adding relations")
-    await ops_test.model.add_relation(KAFKA_APP, ZOOKEEPER_APP)
-    await ops_test.model.add_relation(
-        "{}:mongodb".format(RO_APP), "{}:database".format(MONGO_DB_APP)
-    )
-    await ops_test.model.add_relation(RO_APP, KAFKA_APP)
-
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=APPS,
-            status="active",
-            timeout=300,
-        )
-
-
-@pytest.mark.abort_on_fail
-async def test_ro_scales(ops_test: OpsTest):
-    logger.info("Scaling osm-ro")
-    expected_units = 3
-    assert len(ops_test.model.applications[RO_APP].units) == 1
-    await ops_test.model.applications[RO_APP].scale(expected_units)
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=[RO_APP], status="active", timeout=1000, wait_for_exact_units=expected_units
-        )
-
-
-@pytest.mark.abort_on_fail
-async def test_ro_blocks_without_kafka(ops_test: OpsTest):
-    await asyncio.gather(ops_test.model.applications[KAFKA_APP].remove())
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(apps=[RO_APP])
-    assert ops_test.model.applications[RO_APP].status == "blocked"
-    for unit in ops_test.model.applications[RO_APP].units:
-        assert unit.workload_status_message == "need kafka relation"
diff --git a/installers/charm/osm-ro/tests/unit/test_charm.py b/installers/charm/osm-ro/tests/unit/test_charm.py
deleted file mode 100644 (file)
index d0353ab..0000000
+++ /dev/null
@@ -1,102 +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 limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-#
-# Learn more about testing at: https://juju.is/docs/sdk/testing
-
-import pytest
-from ops.model import ActiveStatus, BlockedStatus
-from ops.testing import Harness
-from pytest_mock import MockerFixture
-
-from charm import CharmError, OsmRoCharm, check_service_active
-
-container_name = "ro"
-service_name = "ro"
-
-
-@pytest.fixture
-def harness(mocker: MockerFixture):
-    mocker.patch("charm.KubernetesServicePatch", lambda x, y: None)
-    harness = Harness(OsmRoCharm)
-    harness.begin()
-    harness.container_pebble_ready(container_name)
-    yield harness
-    harness.cleanup()
-
-
-def test_missing_relations(harness: Harness):
-    harness.charm.on.config_changed.emit()
-    assert type(harness.charm.unit.status) == BlockedStatus
-    assert all(relation in harness.charm.unit.status.message for relation in ["mongodb", "kafka"])
-
-
-def test_ready(harness: Harness):
-    _add_relations(harness)
-    assert harness.charm.unit.status == ActiveStatus()
-
-
-def test_container_stops_after_relation_broken(harness: Harness):
-    harness.charm.on[container_name].pebble_ready.emit(container_name)
-    container = harness.charm.unit.get_container(container_name)
-    relation_ids = _add_relations(harness)
-    check_service_active(container, service_name)
-    harness.remove_relation(relation_ids[0])
-    with pytest.raises(CharmError):
-        check_service_active(container, service_name)
-
-
-def test_ro_relation_joined(harness: Harness):
-    harness.set_leader(True)
-    _add_relations(harness)
-    relation_id = harness.add_relation("ro", "lcm")
-    harness.add_relation_unit(relation_id, "lcm/0")
-    relation_data = harness.get_relation_data(relation_id, harness.charm.app.name)
-    assert harness.charm.unit.status == ActiveStatus()
-    assert relation_data == {"host": harness.charm.app.name, "port": "9090"}
-
-
-def test_certificates(harness: Harness):
-    # aGVsbG8K: "hello\n"
-    # aGVsbG8gYWdhaW4K: "hello again\n"
-    _add_relations(harness)
-    harness.update_config({"certificates": "cert1:aGVsbG8K,cert2:aGVsbG8gYWdhaW4K"})
-    for cert_name, content in {"cert1": "hello\n", "cert2": "hello again\n"}.items():
-        assert harness.charm.container.exists(f"/certs/{cert_name}")
-        assert harness.charm.container.pull(f"/certs/{cert_name}").read() == content
-
-
-def _add_relations(harness: Harness):
-    relation_ids = []
-    # Add mongo relation
-    relation_id = harness.add_relation("mongodb", "mongodb")
-    harness.add_relation_unit(relation_id, "mongodb/0")
-    harness.update_relation_data(
-        relation_id,
-        "mongodb",
-        {"uris": "mongodb://:1234", "username": "user", "password": "password"},
-    )
-    relation_ids.append(relation_id)
-    # Add kafka relation
-    relation_id = harness.add_relation("kafka", "kafka")
-    harness.add_relation_unit(relation_id, "kafka/0")
-    harness.update_relation_data(relation_id, "kafka", {"host": "kafka", "port": "9092"})
-    relation_ids.append(relation_id)
-    return relation_ids
diff --git a/installers/charm/osm-ro/tox.ini b/installers/charm/osm-ro/tox.ini
deleted file mode 100644 (file)
index c6cc629..0000000
+++ /dev/null
@@ -1,95 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-
-[tox]
-skipsdist=True
-skip_missing_interpreters = True
-envlist = lint, unit, integration
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-lib_path = {toxinidir}/lib/charms/osm_ro
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3.8
-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==4.0.1
-    flake8-docstrings
-    flake8-builtins
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-commands =
-    # uncomment the following line if this charm owns a lib
-    codespell {[vars]lib_path} --ignore-words-list=Ro,RO,ro
-    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 --ignore-words-list=Ro,RO,ro
-    # 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:integration]
-description = Run integration tests
-deps =
-    pytest
-    juju<3
-    pytest-operator
-    -r{toxinidir}/requirements.txt
-commands =
-    pytest -v --tb native --ignore={[vars]tst_path}unit --log-cli-level=INFO -s {posargs} --cloud microk8s
\ No newline at end of file
diff --git a/installers/charm/osm-update-db-operator/.gitignore b/installers/charm/osm-update-db-operator/.gitignore
deleted file mode 100644 (file)
index c250157..0000000
+++ /dev/null
@@ -1,23 +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.
-
-venv/
-build/
-*.charm
-.coverage
-coverage.xml
-__pycache__/
-*.py[cod]
-.vscode
-.tox
diff --git a/installers/charm/osm-update-db-operator/.jujuignore b/installers/charm/osm-update-db-operator/.jujuignore
deleted file mode 100644 (file)
index ddb544e..0000000
+++ /dev/null
@@ -1,17 +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.
-
-/venv
-*.py[cod]
-*.charm
diff --git a/installers/charm/osm-update-db-operator/CONTRIBUTING.md b/installers/charm/osm-update-db-operator/CONTRIBUTING.md
deleted file mode 100644 (file)
index 4d70671..0000000
+++ /dev/null
@@ -1,74 +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.
--->
-# Contributing
-
-## Overview
-
-This documents explains the processes and practices recommended for contributing enhancements to
-the Update DB charm.
-
-- Generally, before developing enhancements to this charm, you should consider [opening an issue
-  ](https://github.com/gcalvinos/update-db-operator/issues) explaining your use case.
-- 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/). The primary author of this charm is available on
-  the Mattermost channel as `@davigar15`.
-- 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-update-db
-# Enable DEBUG logging
-juju model-config logging-config="<root>=INFO;unit=DEBUG"
-# Deploy the charm
-juju deploy ./update-db_ubuntu-20.04-amd64.charm \
-  --resource update-db-image=ubuntu:latest
-```
diff --git a/installers/charm/osm-update-db-operator/LICENSE b/installers/charm/osm-update-db-operator/LICENSE
deleted file mode 100644 (file)
index d645695..0000000
+++ /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/installers/charm/osm-update-db-operator/README.md b/installers/charm/osm-update-db-operator/README.md
deleted file mode 100644 (file)
index 2ee8f6e..0000000
+++ /dev/null
@@ -1,80 +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.
--->
-
-# OSM Update DB Operator
-
-[![code style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black/tree/main)
-
-## Description
-
-Charm used to update the OSM databases during an OSM upgrade process. To be used you should have an instance of OSM running that you may want to upgrade
-
-## Usage
-
-### Deploy the charm (locally)
-
-```shell
-juju add-model update-db
-juju deploy osm-update-db-operator --series focal
-```
-
-Set MongoDB and MySQL URIs:
-
-```shell
-juju config osm-update-db-operator mysql-uri=<mysql_uri>
-juju config osm-update-db-operator mongodb-uri=<mongodb_uri>
-```
-
-### Updating the databases
-
-In case we want to update both databases, we need to run the following command:
-
-```shell
-juju run-action osm-update-db-operator/0 update-db current-version=<Number_of_current_version> target-version=<Number_of_target_version>
-# Example:
-juju run-action osm-update-db-operator/0 update-db current-version=9 target-version=10
-```
-
-In case only you just want to update MongoDB, then we can use a flag 'mongodb-only=True':
-
-```shell
-juju run-action osm-update-db-operator/0 update-db current-version=9 target-version=10 mongodb-only=True
-```
-
-In case only you just want to update MySQL database, then we can use a flag 'mysql-only=True':
-
-```shell
-juju run-action osm-update-db-operator/0 update-db current-version=9 target-version=10 mysql-only=True
-```
-
-You can check if the update of the database was properly done checking the result of the command:
-
-```shell
-juju show-action-output <Number_of_the_action>
-```
-
-### Fixes for bugs
-
-Updates de database to apply the changes needed to fix a bug. You need to specify the bug number. Example:
-
-```shell
-juju run-action osm-update-db-operator/0 apply-patch bug-number=1837
-```
-
-## 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/osm-update-db-operator/actions.yaml b/installers/charm/osm-update-db-operator/actions.yaml
deleted file mode 100644 (file)
index aba1ee3..0000000
+++ /dev/null
@@ -1,42 +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.
-
-update-db:
-  description: |
-    Updates the Mongodb and MySQL with the new data needed for the target OSM
-  params:
-    current-version:
-      type: integer
-      description: "Current version of Charmed OSM - Example: 9"
-    target-version:
-      type: integer
-      description: "Final version of OSM after the update - Example: 10"
-    mysql-only:
-      type: boolean
-      description: "if True the update is only applied for mysql database"
-    mongodb-only:
-      type: boolean
-      description: "if True the update is only applied for mongo database"
-  required:
-    - current-version
-    - target-version
-apply-patch:
-  description: |
-    Updates de database to apply the changes needed to fix a bug
-  params:
-    bug-number:
-      type: integer
-      description: "The number of the bug that needs to be fixed"
-  required:
-    - bug-number
diff --git a/installers/charm/osm-update-db-operator/charmcraft.yaml b/installers/charm/osm-update-db-operator/charmcraft.yaml
deleted file mode 100644 (file)
index 31c233b..0000000
+++ /dev/null
@@ -1,26 +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.
-
-type: "charm"
-bases:
-  - build-on:
-      - name: "ubuntu"
-        channel: "20.04"
-    run-on:
-      - name: "ubuntu"
-        channel: "20.04"
-parts:
-  charm:
-    build-packages:
-      - git
diff --git a/installers/charm/osm-update-db-operator/config.yaml b/installers/charm/osm-update-db-operator/config.yaml
deleted file mode 100644 (file)
index 3b7190b..0000000
+++ /dev/null
@@ -1,29 +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.
-
-options:
-  log-level:
-    description: "Log Level"
-    type: string
-    default: "INFO"
-  mongodb-uri:
-    type: string
-    description: |
-      MongoDB URI (external database)
-      mongodb://<mongo_host>:<mongo_port>/
-  mysql-uri:
-    type: string
-    description: |
-      Mysql URI with the following format:
-        mysql://<user>:<password>@<mysql_host>:<mysql_port>/<database>
diff --git a/installers/charm/osm-update-db-operator/metadata.yaml b/installers/charm/osm-update-db-operator/metadata.yaml
deleted file mode 100644 (file)
index b058591..0000000
+++ /dev/null
@@ -1,19 +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.
-
-name: osm-update-db-operator
-description: |
-  Charm to update the OSM databases
-summary: |
-  Charm to update the OSM databases
diff --git a/installers/charm/osm-update-db-operator/pyproject.toml b/installers/charm/osm-update-db-operator/pyproject.toml
deleted file mode 100644 (file)
index 3fae174..0000000
+++ /dev/null
@@ -1,53 +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.
-
-# 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", "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/osm-update-db-operator/requirements.txt b/installers/charm/osm-update-db-operator/requirements.txt
deleted file mode 100644 (file)
index b488dba..0000000
+++ /dev/null
@@ -1,16 +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.
-
-ops < 2.2
-pymongo == 3.12.3
diff --git a/installers/charm/osm-update-db-operator/src/charm.py b/installers/charm/osm-update-db-operator/src/charm.py
deleted file mode 100755 (executable)
index 32db2f7..0000000
+++ /dev/null
@@ -1,119 +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 limitations
-# under the License.
-
-"""Update DB charm module."""
-
-import logging
-
-from ops.charm import CharmBase
-from ops.framework import StoredState
-from ops.main import main
-from ops.model import ActiveStatus, BlockedStatus
-
-from db_upgrade import MongoUpgrade, MysqlUpgrade
-
-logger = logging.getLogger(__name__)
-
-
-class UpgradeDBCharm(CharmBase):
-    """Upgrade DB Charm operator."""
-
-    _stored = StoredState()
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-        # Observe events
-        event_observe_mapping = {
-            self.on.update_db_action: self._on_update_db_action,
-            self.on.apply_patch_action: self._on_apply_patch_action,
-            self.on.config_changed: self._on_config_changed,
-        }
-        for event, observer in event_observe_mapping.items():
-            self.framework.observe(event, observer)
-
-    @property
-    def mongo(self):
-        """Create MongoUpgrade object if the configuration has been set."""
-        mongo_uri = self.config.get("mongodb-uri")
-        return MongoUpgrade(mongo_uri) if mongo_uri else None
-
-    @property
-    def mysql(self):
-        """Create MysqlUpgrade object if the configuration has been set."""
-        mysql_uri = self.config.get("mysql-uri")
-        return MysqlUpgrade(mysql_uri) if mysql_uri else None
-
-    def _on_config_changed(self, _):
-        mongo_uri = self.config.get("mongodb-uri")
-        mysql_uri = self.config.get("mysql-uri")
-        if not mongo_uri and not mysql_uri:
-            self.unit.status = BlockedStatus("mongodb-uri and/or mysql-uri must be set")
-            return
-        self.unit.status = ActiveStatus()
-
-    def _on_update_db_action(self, event):
-        """Handle the update-db action."""
-        current_version = str(event.params["current-version"])
-        target_version = str(event.params["target-version"])
-        mysql_only = event.params.get("mysql-only")
-        mongodb_only = event.params.get("mongodb-only")
-        try:
-            results = {}
-            if mysql_only and mongodb_only:
-                raise Exception("cannot set both mysql-only and mongodb-only options to True")
-            if mysql_only:
-                self._upgrade_mysql(current_version, target_version)
-                results["mysql"] = "Upgraded successfully"
-            elif mongodb_only:
-                self._upgrade_mongodb(current_version, target_version)
-                results["mongodb"] = "Upgraded successfully"
-            else:
-                self._upgrade_mysql(current_version, target_version)
-                results["mysql"] = "Upgraded successfully"
-                self._upgrade_mongodb(current_version, target_version)
-                results["mongodb"] = "Upgraded successfully"
-            event.set_results(results)
-        except Exception as e:
-            event.fail(f"Failed DB Upgrade: {e}")
-
-    def _upgrade_mysql(self, current_version, target_version):
-        logger.debug("Upgrading mysql")
-        if self.mysql:
-            self.mysql.upgrade(current_version, target_version)
-        else:
-            raise Exception("mysql-uri not set")
-
-    def _upgrade_mongodb(self, current_version, target_version):
-        logger.debug("Upgrading mongodb")
-        if self.mongo:
-            self.mongo.upgrade(current_version, target_version)
-        else:
-            raise Exception("mongo-uri not set")
-
-    def _on_apply_patch_action(self, event):
-        bug_number = event.params["bug-number"]
-        logger.debug("Patching bug number {}".format(str(bug_number)))
-        try:
-            if self.mongo:
-                self.mongo.apply_patch(bug_number)
-            else:
-                raise Exception("mongo-uri not set")
-        except Exception as e:
-            event.fail(f"Failed Patch Application: {e}")
-
-
-if __name__ == "__main__":  # pragma: no cover
-    main(UpgradeDBCharm, use_juju_for_storage=True)
diff --git a/installers/charm/osm-update-db-operator/src/db_upgrade.py b/installers/charm/osm-update-db-operator/src/db_upgrade.py
deleted file mode 100644 (file)
index 295ce87..0000000
+++ /dev/null
@@ -1,542 +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.
-
-"""Upgrade DB charm module."""
-
-import json
-import logging
-
-from pymongo import MongoClient
-from uuid import uuid4
-
-logger = logging.getLogger(__name__)
-
-
-class MongoUpgrade1214:
-    """Upgrade MongoDB Database from OSM v12 to v14."""
-
-    @staticmethod
-    def gather_vnfr_healing_alerts(vnfr, vnfd):
-        alerts = []
-        nsr_id = vnfr["nsr-id-ref"]
-        df = vnfd.get("df", [{}])[0]
-        # Checking for auto-healing configuration
-        if "healing-aspect" in df:
-            healing_aspects = df["healing-aspect"]
-            for healing in healing_aspects:
-                for healing_policy in healing.get("healing-policy", ()):
-                    vdu_id = healing_policy["vdu-id"]
-                    vdur = next(
-                        (
-                            vdur
-                            for vdur in vnfr["vdur"]
-                            if vdu_id == vdur["vdu-id-ref"]
-                        ),
-                        {},
-                    )
-                    if not vdur:
-                        continue
-                    metric_name = "vm_status"
-                    vdu_name = vdur.get("name")
-                    vnf_member_index = vnfr["member-vnf-index-ref"]
-                    uuid = str(uuid4())
-                    name = f"healing_{uuid}"
-                    action = healing_policy
-                    # action_on_recovery = healing.get("action-on-recovery")
-                    # cooldown_time = healing.get("cooldown-time")
-                    # day1 = healing.get("day1")
-                    alert = {
-                        "uuid": uuid,
-                        "name": name,
-                        "metric": metric_name,
-                        "tags": {
-                            "ns_id": nsr_id,
-                            "vnf_member_index": vnf_member_index,
-                            "vdu_name": vdu_name,
-                        },
-                        "alarm_status": "ok",
-                        "action_type": "healing",
-                        "action": action,
-                    }
-                    alerts.append(alert)
-        return alerts
-
-    @staticmethod
-    def gather_vnfr_scaling_alerts(vnfr, vnfd):
-        alerts = []
-        nsr_id = vnfr["nsr-id-ref"]
-        df = vnfd.get("df", [{}])[0]
-        # Checking for auto-scaling configuration
-        if "scaling-aspect" in df:
-            rel_operation_types = {
-                "GE": ">=",
-                "LE": "<=",
-                "GT": ">",
-                "LT": "<",
-                "EQ": "==",
-                "NE": "!=",
-            }
-            scaling_aspects = df["scaling-aspect"]
-            all_vnfd_monitoring_params = {}
-            for ivld in vnfd.get("int-virtual-link-desc", ()):
-                for mp in ivld.get("monitoring-parameters", ()):
-                    all_vnfd_monitoring_params[mp.get("id")] = mp
-            for vdu in vnfd.get("vdu", ()):
-                for mp in vdu.get("monitoring-parameter", ()):
-                    all_vnfd_monitoring_params[mp.get("id")] = mp
-            for df in vnfd.get("df", ()):
-                for mp in df.get("monitoring-parameter", ()):
-                    all_vnfd_monitoring_params[mp.get("id")] = mp
-            for scaling_aspect in scaling_aspects:
-                scaling_group_name = scaling_aspect.get("name", "")
-                # Get monitored VDUs
-                all_monitored_vdus = set()
-                for delta in scaling_aspect.get(
-                    "aspect-delta-details", {}
-                ).get("deltas", ()):
-                    for vdu_delta in delta.get("vdu-delta", ()):
-                        all_monitored_vdus.add(vdu_delta.get("id"))
-                monitored_vdurs = list(
-                    filter(
-                        lambda vdur: vdur["vdu-id-ref"]
-                        in all_monitored_vdus,
-                        vnfr["vdur"],
-                    )
-                )
-                if not monitored_vdurs:
-                    logger.error("Scaling criteria is referring to a vnf-monitoring-param that does not contain a reference to a vdu or vnf metric")
-                    continue
-                for scaling_policy in scaling_aspect.get(
-                    "scaling-policy", ()
-                ):
-                    if scaling_policy["scaling-type"] != "automatic":
-                        continue
-                    threshold_time = scaling_policy.get(
-                        "threshold-time", "1"
-                    )
-                    cooldown_time = scaling_policy.get("cooldown-time", "0")
-                    for scaling_criteria in scaling_policy["scaling-criteria"]:
-                        monitoring_param_ref = scaling_criteria.get(
-                            "vnf-monitoring-param-ref"
-                        )
-                        vnf_monitoring_param = all_vnfd_monitoring_params[
-                            monitoring_param_ref
-                        ]
-                        for vdur in monitored_vdurs:
-                            vdu_id = vdur["vdu-id-ref"]
-                            metric_name = vnf_monitoring_param.get("performance-metric")
-                            metric_name = f"osm_{metric_name}"
-                            vdu_name = vdur["name"]
-                            vnf_member_index = vnfr["member-vnf-index-ref"]
-                            scalein_threshold = scaling_criteria.get("scale-in-threshold")
-                            # Looking for min/max-number-of-instances
-                            instances_min_number = 1
-                            instances_max_number = 1
-                            vdu_profile = df["vdu-profile"]
-                            if vdu_profile:
-                                profile = next(
-                                    item
-                                    for item in vdu_profile
-                                    if item["id"] == vdu_id
-                                )
-                                instances_min_number = profile.get("min-number-of-instances", 1)
-                                instances_max_number = profile.get("max-number-of-instances", 1)
-
-                            if scalein_threshold:
-                                uuid = str(uuid4())
-                                name = f"scalein_{uuid}"
-                                operation = scaling_criteria["scale-in-relational-operation"]
-                                rel_operator = rel_operation_types.get(operation, "<=")
-                                metric_selector = f'{metric_name}{{ns_id="{nsr_id}", vnf_member_index="{vnf_member_index}", vdu_id="{vdu_id}"}}'
-                                expression = f"(count ({metric_selector}) > {instances_min_number}) and (avg({metric_selector}) {rel_operator} {scalein_threshold})"
-                                labels = {
-                                    "ns_id": nsr_id,
-                                    "vnf_member_index": vnf_member_index,
-                                    "vdu_id": vdu_id,
-                                }
-                                prom_cfg = {
-                                    "alert": name,
-                                    "expr": expression,
-                                    "for": str(threshold_time) + "m",
-                                    "labels": labels,
-                                }
-                                action = scaling_policy
-                                action = {
-                                    "scaling-group": scaling_group_name,
-                                    "cooldown-time": cooldown_time,
-                                }
-                                alert = {
-                                    "uuid": uuid,
-                                    "name": name,
-                                    "metric": metric_name,
-                                    "tags": {
-                                        "ns_id": nsr_id,
-                                        "vnf_member_index": vnf_member_index,
-                                        "vdu_id": vdu_id,
-                                    },
-                                    "alarm_status": "ok",
-                                    "action_type": "scale_in",
-                                    "action": action,
-                                    "prometheus_config": prom_cfg,
-                                }
-                                alerts.append(alert)
-
-                            scaleout_threshold = scaling_criteria.get("scale-out-threshold")
-                            if scaleout_threshold:
-                                uuid = str(uuid4())
-                                name = f"scaleout_{uuid}"
-                                operation = scaling_criteria["scale-out-relational-operation"]
-                                rel_operator = rel_operation_types.get(operation, "<=")
-                                metric_selector = f'{metric_name}{{ns_id="{nsr_id}", vnf_member_index="{vnf_member_index}", vdu_id="{vdu_id}"}}'
-                                expression = f"(count ({metric_selector}) < {instances_max_number}) and (avg({metric_selector}) {rel_operator} {scaleout_threshold})"
-                                labels = {
-                                    "ns_id": nsr_id,
-                                    "vnf_member_index": vnf_member_index,
-                                    "vdu_id": vdu_id,
-                                }
-                                prom_cfg = {
-                                    "alert": name,
-                                    "expr": expression,
-                                    "for": str(threshold_time) + "m",
-                                    "labels": labels,
-                                }
-                                action = scaling_policy
-                                action = {
-                                    "scaling-group": scaling_group_name,
-                                    "cooldown-time": cooldown_time,
-                                }
-                                alert = {
-                                    "uuid": uuid,
-                                    "name": name,
-                                    "metric": metric_name,
-                                    "tags": {
-                                        "ns_id": nsr_id,
-                                        "vnf_member_index": vnf_member_index,
-                                        "vdu_id": vdu_id,
-                                    },
-                                    "alarm_status": "ok",
-                                    "action_type": "scale_out",
-                                    "action": action,
-                                    "prometheus_config": prom_cfg,
-                                }
-                                alerts.append(alert)
-        return alerts
-
-    @staticmethod
-    def _migrate_alerts(osm_db):
-        """Create new alerts collection.
-        """
-        if "alerts" in osm_db.list_collection_names():
-            return
-        logger.info("Entering in MongoUpgrade1214._migrate_alerts function")
-
-        # Get vnfds from MongoDB
-        logger.info("Reading VNF descriptors:")
-        vnfds = osm_db["vnfds"]
-        db_vnfds = []
-        for vnfd in vnfds.find():
-            logger.info(f'  {vnfd["_id"]}: {vnfd["description"]}')
-            db_vnfds.append(vnfd)
-
-        # Get vnfrs from MongoDB
-        logger.info("Reading VNFRs")
-        vnfrs = osm_db["vnfrs"]
-
-        # Gather healing and scaling alerts for each vnfr
-        healing_alerts = []
-        scaling_alerts = []
-        for vnfr in vnfrs.find():
-            logger.info(f'  vnfr {vnfr["_id"]}')
-            vnfd = next((sub for sub in db_vnfds if sub["_id"] == vnfr["vnfd-id"]), None)
-            healing_alerts.extend(MongoUpgrade1214.gather_vnfr_healing_alerts(vnfr, vnfd))
-            scaling_alerts.extend(MongoUpgrade1214.gather_vnfr_scaling_alerts(vnfr, vnfd))
-
-        # Add new alerts in MongoDB
-        alerts = osm_db["alerts"]
-        for alert in healing_alerts:
-            logger.info(f"Storing healing alert in MongoDB: {alert}")
-            alerts.insert_one(alert)
-        for alert in scaling_alerts:
-            logger.info(f"Storing scaling alert in MongoDB: {alert}")
-            alerts.insert_one(alert)
-
-        # Delete old alarms collections
-        logger.info("Deleting alarms and alarms_action collections")
-        alarms = osm_db["alarms"]
-        alarms.drop()
-        alarms_action = osm_db["alarms_action"]
-        alarms_action.drop()
-
-
-    @staticmethod
-    def upgrade(mongo_uri):
-        """Upgrade alerts in MongoDB."""
-        logger.info("Entering in MongoUpgrade1214.upgrade function")
-        myclient = MongoClient(mongo_uri)
-        osm_db = myclient["osm"]
-        MongoUpgrade1214._migrate_alerts(osm_db)
-
-
-class MongoUpgrade1012:
-    """Upgrade MongoDB Database from OSM v10 to v12."""
-
-    @staticmethod
-    def _remove_namespace_from_k8s(nsrs, nsr):
-        namespace = "kube-system:"
-        if nsr["_admin"].get("deployed"):
-            k8s_list = []
-            for k8s in nsr["_admin"]["deployed"].get("K8s"):
-                if k8s.get("k8scluster-uuid"):
-                    k8s["k8scluster-uuid"] = k8s["k8scluster-uuid"].replace(namespace, "", 1)
-                k8s_list.append(k8s)
-            myquery = {"_id": nsr["_id"]}
-            nsrs.update_one(myquery, {"$set": {"_admin.deployed.K8s": k8s_list}})
-
-    @staticmethod
-    def _update_nsr(osm_db):
-        """Update nsr.
-
-        Add vim_message = None if it does not exist.
-        Remove "namespace:" from k8scluster-uuid.
-        """
-        if "nsrs" not in osm_db.list_collection_names():
-            return
-        logger.info("Entering in MongoUpgrade1012._update_nsr function")
-
-        nsrs = osm_db["nsrs"]
-        for nsr in nsrs.find():
-            logger.debug(f"Updating {nsr['_id']} nsr")
-            for key, values in nsr.items():
-                if isinstance(values, list):
-                    item_list = []
-                    for value in values:
-                        if isinstance(value, dict) and value.get("vim_info"):
-                            index = list(value["vim_info"].keys())[0]
-                            if not value["vim_info"][index].get("vim_message"):
-                                value["vim_info"][index]["vim_message"] = None
-                            item_list.append(value)
-                    myquery = {"_id": nsr["_id"]}
-                    nsrs.update_one(myquery, {"$set": {key: item_list}})
-            MongoUpgrade1012._remove_namespace_from_k8s(nsrs, nsr)
-
-    @staticmethod
-    def _update_vnfr(osm_db):
-        """Update vnfr.
-
-        Add vim_message to vdur if it does not exist.
-        Copy content of interfaces into interfaces_backup.
-        """
-        if "vnfrs" not in osm_db.list_collection_names():
-            return
-        logger.info("Entering in MongoUpgrade1012._update_vnfr function")
-        mycol = osm_db["vnfrs"]
-        for vnfr in mycol.find():
-            logger.debug(f"Updating {vnfr['_id']} vnfr")
-            vdur_list = []
-            for vdur in vnfr["vdur"]:
-                if vdur.get("vim_info"):
-                    index = list(vdur["vim_info"].keys())[0]
-                    if not vdur["vim_info"][index].get("vim_message"):
-                        vdur["vim_info"][index]["vim_message"] = None
-                    if vdur["vim_info"][index].get(
-                        "interfaces", "Not found"
-                    ) != "Not found" and not vdur["vim_info"][index].get("interfaces_backup"):
-                        vdur["vim_info"][index]["interfaces_backup"] = vdur["vim_info"][index][
-                            "interfaces"
-                        ]
-                vdur_list.append(vdur)
-            myquery = {"_id": vnfr["_id"]}
-            mycol.update_one(myquery, {"$set": {"vdur": vdur_list}})
-
-    @staticmethod
-    def _update_k8scluster(osm_db):
-        """Remove namespace from helm-chart and helm-chart-v3 id."""
-        if "k8sclusters" not in osm_db.list_collection_names():
-            return
-        logger.info("Entering in MongoUpgrade1012._update_k8scluster function")
-        namespace = "kube-system:"
-        k8sclusters = osm_db["k8sclusters"]
-        for k8scluster in k8sclusters.find():
-            if k8scluster["_admin"].get("helm-chart") and k8scluster["_admin"]["helm-chart"].get(
-                "id"
-            ):
-                if k8scluster["_admin"]["helm-chart"]["id"].startswith(namespace):
-                    k8scluster["_admin"]["helm-chart"]["id"] = k8scluster["_admin"]["helm-chart"][
-                        "id"
-                    ].replace(namespace, "", 1)
-            if k8scluster["_admin"].get("helm-chart-v3") and k8scluster["_admin"][
-                "helm-chart-v3"
-            ].get("id"):
-                if k8scluster["_admin"]["helm-chart-v3"]["id"].startswith(namespace):
-                    k8scluster["_admin"]["helm-chart-v3"]["id"] = k8scluster["_admin"][
-                        "helm-chart-v3"
-                    ]["id"].replace(namespace, "", 1)
-            myquery = {"_id": k8scluster["_id"]}
-            k8sclusters.update_one(myquery, {"$set": k8scluster})
-
-    @staticmethod
-    def upgrade(mongo_uri):
-        """Upgrade nsr, vnfr and k8scluster in DB."""
-        logger.info("Entering in MongoUpgrade1012.upgrade function")
-        myclient = MongoClient(mongo_uri)
-        osm_db = myclient["osm"]
-        MongoUpgrade1012._update_nsr(osm_db)
-        MongoUpgrade1012._update_vnfr(osm_db)
-        MongoUpgrade1012._update_k8scluster(osm_db)
-
-
-class MongoUpgrade910:
-    """Upgrade MongoDB Database from OSM v9 to v10."""
-
-    @staticmethod
-    def upgrade(mongo_uri):
-        """Add parameter alarm status = OK if not found in alarms collection."""
-        myclient = MongoClient(mongo_uri)
-        osm_db = myclient["osm"]
-        collist = osm_db.list_collection_names()
-
-        if "alarms" in collist:
-            mycol = osm_db["alarms"]
-            for x in mycol.find():
-                if not x.get("alarm_status"):
-                    myquery = {"_id": x["_id"]}
-                    mycol.update_one(myquery, {"$set": {"alarm_status": "ok"}})
-
-
-class MongoPatch1837:
-    """Patch Bug 1837 on MongoDB."""
-
-    @staticmethod
-    def _update_nslcmops_params(osm_db):
-        """Updates the nslcmops collection to change the additional params to a string."""
-        logger.info("Entering in MongoPatch1837._update_nslcmops_params function")
-        if "nslcmops" in osm_db.list_collection_names():
-            nslcmops = osm_db["nslcmops"]
-            for nslcmop in nslcmops.find():
-                if nslcmop.get("operationParams"):
-                    if nslcmop["operationParams"].get("additionalParamsForVnf") and isinstance(
-                        nslcmop["operationParams"].get("additionalParamsForVnf"), list
-                    ):
-                        string_param = json.dumps(
-                            nslcmop["operationParams"]["additionalParamsForVnf"]
-                        )
-                        myquery = {"_id": nslcmop["_id"]}
-                        nslcmops.update_one(
-                            myquery,
-                            {
-                                "$set": {
-                                    "operationParams": {"additionalParamsForVnf": string_param}
-                                }
-                            },
-                        )
-                    elif nslcmop["operationParams"].get("primitive_params") and isinstance(
-                        nslcmop["operationParams"].get("primitive_params"), dict
-                    ):
-                        string_param = json.dumps(nslcmop["operationParams"]["primitive_params"])
-                        myquery = {"_id": nslcmop["_id"]}
-                        nslcmops.update_one(
-                            myquery,
-                            {"$set": {"operationParams": {"primitive_params": string_param}}},
-                        )
-
-    @staticmethod
-    def _update_vnfrs_params(osm_db):
-        """Updates the vnfrs collection to change the additional params to a string."""
-        logger.info("Entering in MongoPatch1837._update_vnfrs_params function")
-        if "vnfrs" in osm_db.list_collection_names():
-            mycol = osm_db["vnfrs"]
-            for vnfr in mycol.find():
-                if vnfr.get("kdur"):
-                    kdur_list = []
-                    for kdur in vnfr["kdur"]:
-                        if kdur.get("additionalParams") and not isinstance(
-                            kdur["additionalParams"], str
-                        ):
-                            kdur["additionalParams"] = json.dumps(kdur["additionalParams"])
-                        kdur_list.append(kdur)
-                    myquery = {"_id": vnfr["_id"]}
-                    mycol.update_one(
-                        myquery,
-                        {"$set": {"kdur": kdur_list}},
-                    )
-                    vnfr["kdur"] = kdur_list
-
-    @staticmethod
-    def patch(mongo_uri):
-        """Updates the database to change the additional params from dict to a string."""
-        logger.info("Entering in MongoPatch1837.patch function")
-        myclient = MongoClient(mongo_uri)
-        osm_db = myclient["osm"]
-        MongoPatch1837._update_nslcmops_params(osm_db)
-        MongoPatch1837._update_vnfrs_params(osm_db)
-
-
-MONGODB_UPGRADE_FUNCTIONS = {
-    "9": {"10": [MongoUpgrade910.upgrade]},
-    "10": {"12": [MongoUpgrade1012.upgrade]},
-    "12": {"14": [MongoUpgrade1214.upgrade]},
-}
-MYSQL_UPGRADE_FUNCTIONS = {}
-BUG_FIXES = {
-    1837: MongoPatch1837.patch,
-}
-
-
-class MongoUpgrade:
-    """Upgrade MongoDB Database."""
-
-    def __init__(self, mongo_uri):
-        self.mongo_uri = mongo_uri
-
-    def upgrade(self, current, target):
-        """Validates the upgrading path and upgrades the DB."""
-        self._validate_upgrade(current, target)
-        for function in MONGODB_UPGRADE_FUNCTIONS.get(current)[target]:
-            function(self.mongo_uri)
-
-    def _validate_upgrade(self, current, target):
-        """Check if the upgrade path chosen is possible."""
-        logger.info("Validating the upgrade path")
-        if current not in MONGODB_UPGRADE_FUNCTIONS:
-            raise Exception(f"cannot upgrade from {current} version.")
-        if target not in MONGODB_UPGRADE_FUNCTIONS[current]:
-            raise Exception(f"cannot upgrade from version {current} to {target}.")
-
-    def apply_patch(self, bug_number: int) -> None:
-        """Checks the bug-number and applies the fix in the database."""
-        if bug_number not in BUG_FIXES:
-            raise Exception(f"There is no patch for bug {bug_number}")
-        patch_function = BUG_FIXES[bug_number]
-        patch_function(self.mongo_uri)
-
-
-class MysqlUpgrade:
-    """Upgrade Mysql Database."""
-
-    def __init__(self, mysql_uri):
-        self.mysql_uri = mysql_uri
-
-    def upgrade(self, current, target):
-        """Validates the upgrading path and upgrades the DB."""
-        self._validate_upgrade(current, target)
-        for function in MYSQL_UPGRADE_FUNCTIONS[current][target]:
-            function(self.mysql_uri)
-
-    def _validate_upgrade(self, current, target):
-        """Check if the upgrade path chosen is possible."""
-        logger.info("Validating the upgrade path")
-        if current not in MYSQL_UPGRADE_FUNCTIONS:
-            raise Exception(f"cannot upgrade from {current} version.")
-        if target not in MYSQL_UPGRADE_FUNCTIONS[current]:
-            raise Exception(f"cannot upgrade from version {current} to {target}.")
diff --git a/installers/charm/osm-update-db-operator/tests/integration/test_charm.py b/installers/charm/osm-update-db-operator/tests/integration/test_charm.py
deleted file mode 100644 (file)
index cc9e0be..0000000
+++ /dev/null
@@ -1,48 +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.
-
-import base64
-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-under-test 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"})
-    # build and deploy charm from local source folder
-    charm = await ops_test.build_charm(".")
-    resources = {
-        "update-db-image": METADATA["resources"]["update-db-image"]["upstream-source"],
-    }
-    await ops_test.model.deploy(charm, resources=resources, application_name="update-db")
-    await ops_test.model.wait_for_idle(apps=["update-db"], status="active", timeout=1000)
-    assert ops_test.model.applications["update-db"].units[0].workload_status == "active"
-
-    await ops_test.model.set_config({"update-status-hook-interval": "60m"})
-
-
-def base64_encode(phrase: str) -> str:
-    return base64.b64encode(phrase.encode("utf-8")).decode("utf-8")
diff --git a/installers/charm/osm-update-db-operator/tests/unit/test_charm.py b/installers/charm/osm-update-db-operator/tests/unit/test_charm.py
deleted file mode 100644 (file)
index a0f625d..0000000
+++ /dev/null
@@ -1,165 +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.
-
-import unittest
-from unittest.mock import Mock, patch
-
-from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus
-from ops.testing import Harness
-
-from charm import UpgradeDBCharm
-
-
-class TestCharm(unittest.TestCase):
-    def setUp(self):
-        self.harness = Harness(UpgradeDBCharm)
-        self.addCleanup(self.harness.cleanup)
-        self.harness.begin()
-
-    def test_initial_config(self):
-        self.assertEqual(self.harness.model.unit.status, MaintenanceStatus(""))
-
-    def test_config_changed(self):
-        self.harness.update_config({"mongodb-uri": "foo"})
-        self.assertEqual(self.harness.model.unit.status, ActiveStatus())
-
-    def test_config_changed_blocked(self):
-        self.harness.update_config({"log-level": "DEBUG"})
-        self.assertEqual(
-            self.harness.model.unit.status,
-            BlockedStatus("mongodb-uri and/or mysql-uri must be set"),
-        )
-
-    def test_update_db_fail_only_params(self):
-        action_event = Mock(
-            params={
-                "current-version": 9,
-                "target-version": 10,
-                "mysql-only": True,
-                "mongodb-only": True,
-            }
-        )
-        self.harness.charm._on_update_db_action(action_event)
-        self.assertEqual(
-            action_event.fail.call_args,
-            [("Failed DB Upgrade: cannot set both mysql-only and mongodb-only options to True",)],
-        )
-
-    @patch("charm.MongoUpgrade")
-    @patch("charm.MysqlUpgrade")
-    def test_update_db_mysql(self, mock_mysql_upgrade, mock_mongo_upgrade):
-        self.harness.update_config({"mysql-uri": "foo"})
-        action_event = Mock(
-            params={
-                "current-version": 9,
-                "target-version": 10,
-                "mysql-only": True,
-                "mongodb-only": False,
-            }
-        )
-        self.harness.charm._on_update_db_action(action_event)
-        mock_mysql_upgrade().upgrade.assert_called_once()
-        mock_mongo_upgrade.assert_not_called()
-
-    @patch("charm.MongoUpgrade")
-    @patch("charm.MysqlUpgrade")
-    def test_update_db_mongo(self, mock_mysql_upgrade, mock_mongo_upgrade):
-        self.harness.update_config({"mongodb-uri": "foo"})
-        action_event = Mock(
-            params={
-                "current-version": 7,
-                "target-version": 10,
-                "mysql-only": False,
-                "mongodb-only": True,
-            }
-        )
-        self.harness.charm._on_update_db_action(action_event)
-        mock_mongo_upgrade().upgrade.assert_called_once()
-        mock_mysql_upgrade.assert_not_called()
-
-    @patch("charm.MongoUpgrade")
-    def test_update_db_not_configured_mongo_fail(self, mock_mongo_upgrade):
-        action_event = Mock(
-            params={
-                "current-version": 7,
-                "target-version": 10,
-                "mysql-only": False,
-                "mongodb-only": True,
-            }
-        )
-        self.harness.charm._on_update_db_action(action_event)
-        mock_mongo_upgrade.assert_not_called()
-        self.assertEqual(
-            action_event.fail.call_args,
-            [("Failed DB Upgrade: mongo-uri not set",)],
-        )
-
-    @patch("charm.MysqlUpgrade")
-    def test_update_db_not_configured_mysql_fail(self, mock_mysql_upgrade):
-        action_event = Mock(
-            params={
-                "current-version": 7,
-                "target-version": 10,
-                "mysql-only": True,
-                "mongodb-only": False,
-            }
-        )
-        self.harness.charm._on_update_db_action(action_event)
-        mock_mysql_upgrade.assert_not_called()
-        self.assertEqual(
-            action_event.fail.call_args,
-            [("Failed DB Upgrade: mysql-uri not set",)],
-        )
-
-    @patch("charm.MongoUpgrade")
-    @patch("charm.MysqlUpgrade")
-    def test_update_db_mongodb_and_mysql(self, mock_mysql_upgrade, mock_mongo_upgrade):
-        self.harness.update_config({"mongodb-uri": "foo"})
-        self.harness.update_config({"mysql-uri": "foo"})
-        action_event = Mock(
-            params={
-                "current-version": 7,
-                "target-version": 10,
-                "mysql-only": False,
-                "mongodb-only": False,
-            }
-        )
-        self.harness.charm._on_update_db_action(action_event)
-        mock_mysql_upgrade().upgrade.assert_called_once()
-        mock_mongo_upgrade().upgrade.assert_called_once()
-
-    @patch("charm.MongoUpgrade")
-    def test_apply_patch(self, mock_mongo_upgrade):
-        self.harness.update_config({"mongodb-uri": "foo"})
-        action_event = Mock(
-            params={
-                "bug-number": 57,
-            }
-        )
-        self.harness.charm._on_apply_patch_action(action_event)
-        mock_mongo_upgrade().apply_patch.assert_called_once()
-
-    @patch("charm.MongoUpgrade")
-    def test_apply_patch_fail(self, mock_mongo_upgrade):
-        action_event = Mock(
-            params={
-                "bug-number": 57,
-            }
-        )
-        self.harness.charm._on_apply_patch_action(action_event)
-        mock_mongo_upgrade.assert_not_called()
-        self.assertEqual(
-            action_event.fail.call_args,
-            [("Failed Patch Application: mongo-uri not set",)],
-        )
diff --git a/installers/charm/osm-update-db-operator/tests/unit/test_db_upgrade.py b/installers/charm/osm-update-db-operator/tests/unit/test_db_upgrade.py
deleted file mode 100644 (file)
index 50affdd..0000000
+++ /dev/null
@@ -1,413 +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.
-
-import logging
-import unittest
-from unittest.mock import MagicMock, Mock, call, patch
-
-import db_upgrade
-from db_upgrade import (
-    MongoPatch1837,
-    MongoUpgrade,
-    MongoUpgrade910,
-    MongoUpgrade1012,
-    MysqlUpgrade,
-)
-
-logger = logging.getLogger(__name__)
-
-
-class TestUpgradeMongo910(unittest.TestCase):
-    @patch("db_upgrade.MongoClient")
-    def test_upgrade_mongo_9_10(self, mock_mongo_client):
-        mock_db = MagicMock()
-        alarms = Mock()
-        alarms.find.return_value = [{"_id": "1", "alarm_status": "1"}]
-        collection_dict = {"alarms": alarms, "other": {}}
-        mock_db.list_collection_names.return_value = collection_dict
-        mock_db.__getitem__.side_effect = collection_dict.__getitem__
-        mock_mongo_client.return_value = {"osm": mock_db}
-        MongoUpgrade910.upgrade("mongo_uri")
-        alarms.update_one.assert_not_called()
-
-    @patch("db_upgrade.MongoClient")
-    def test_upgrade_mongo_9_10_no_alarms(self, mock_mongo_client):
-        mock_db = Mock()
-        mock_db.__getitem__ = Mock()
-
-        mock_db.list_collection_names.return_value = {"other": {}}
-        mock_db.alarms.return_value = None
-        mock_mongo_client.return_value = {"osm": mock_db}
-        self.assertIsNone(MongoUpgrade910.upgrade("mongo_uri"))
-
-    @patch("db_upgrade.MongoClient")
-    def test_upgrade_mongo_9_10_no_alarm_status(self, mock_mongo_client):
-        mock_db = MagicMock()
-        alarms = Mock()
-        alarms.find.return_value = [{"_id": "1"}]
-        collection_dict = {"alarms": alarms, "other": {}}
-        mock_db.list_collection_names.return_value = collection_dict
-        mock_db.__getitem__.side_effect = collection_dict.__getitem__
-        mock_db.alarms.return_value = alarms
-        mock_mongo_client.return_value = {"osm": mock_db}
-        MongoUpgrade910.upgrade("mongo_uri")
-        alarms.update_one.assert_called_once_with({"_id": "1"}, {"$set": {"alarm_status": "ok"}})
-
-
-class TestUpgradeMongo1012(unittest.TestCase):
-    def setUp(self):
-        self.mock_db = MagicMock()
-        self.nsrs = Mock()
-        self.vnfrs = Mock()
-        self.k8s_clusters = Mock()
-
-    @patch("db_upgrade.MongoClient")
-    def test_update_nsr_empty_nsrs(self, mock_mongo_client):
-        self.nsrs.find.return_value = []
-        collection_list = {"nsrs": self.nsrs}
-        self.mock_db.__getitem__.side_effect = collection_list.__getitem__
-        self.mock_db.list_collection_names.return_value = collection_list
-        mock_mongo_client.return_value = {"osm": self.mock_db}
-        MongoUpgrade1012.upgrade("mongo_uri")
-
-    @patch("db_upgrade.MongoClient")
-    def test_update_nsr_empty_nsr(self, mock_mongo_client):
-        nsr = MagicMock()
-        nsr_values = {"_id": "2", "_admin": {}}
-        nsr.__getitem__.side_effect = nsr_values.__getitem__
-        nsr.items.return_value = []
-        self.nsrs.find.return_value = [nsr]
-        collection_list = {"nsrs": self.nsrs}
-        self.mock_db.__getitem__.side_effect = collection_list.__getitem__
-        self.mock_db.list_collection_names.return_value = collection_list
-        mock_mongo_client.return_value = {"osm": self.mock_db}
-        MongoUpgrade1012.upgrade("mongo_uri")
-
-    @patch("db_upgrade.MongoClient")
-    def test_update_nsr_add_vim_message(self, mock_mongo_client):
-        nsr = MagicMock()
-        vim_info1 = {"vim_info_key1": {}}
-        vim_info2 = {"vim_info_key2": {"vim_message": "Hello"}}
-        nsr_items = {"nsr_item_key": [{"vim_info": vim_info1}, {"vim_info": vim_info2}]}
-        nsr_values = {"_id": "2", "_admin": {}}
-        nsr.__getitem__.side_effect = nsr_values.__getitem__
-        nsr.items.return_value = nsr_items.items()
-        self.nsrs.find.return_value = [nsr]
-        collection_list = {"nsrs": self.nsrs}
-        self.mock_db.__getitem__.side_effect = collection_list.__getitem__
-        self.mock_db.list_collection_names.return_value = collection_list
-        mock_mongo_client.return_value = {"osm": self.mock_db}
-        MongoUpgrade1012.upgrade("mongo_uri")
-        expected_vim_info = {"vim_info_key1": {"vim_message": None}}
-        expected_vim_info2 = {"vim_info_key2": {"vim_message": "Hello"}}
-        self.assertEqual(vim_info1, expected_vim_info)
-        self.assertEqual(vim_info2, expected_vim_info2)
-        self.nsrs.update_one.assert_called_once_with({"_id": "2"}, {"$set": nsr_items})
-
-    @patch("db_upgrade.MongoClient")
-    def test_update_nsr_admin(self, mock_mongo_client):
-        nsr = MagicMock()
-        k8s = [{"k8scluster-uuid": "namespace"}, {"k8scluster-uuid": "kube-system:k8s"}]
-        admin = {"deployed": {"K8s": k8s}}
-        nsr_values = {"_id": "2", "_admin": admin}
-        nsr.__getitem__.side_effect = nsr_values.__getitem__
-        nsr_items = {}
-        nsr.items.return_value = nsr_items.items()
-        self.nsrs.find.return_value = [nsr]
-        collection_list = {"nsrs": self.nsrs}
-        self.mock_db.__getitem__.side_effect = collection_list.__getitem__
-        self.mock_db.list_collection_names.return_value = collection_list
-        mock_mongo_client.return_value = {"osm": self.mock_db}
-        MongoUpgrade1012.upgrade("mongo_uri")
-        expected_k8s = [{"k8scluster-uuid": "namespace"}, {"k8scluster-uuid": "k8s"}]
-        self.nsrs.update_one.assert_called_once_with(
-            {"_id": "2"}, {"$set": {"_admin.deployed.K8s": expected_k8s}}
-        )
-
-    @patch("db_upgrade.MongoClient")
-    def test_update_vnfr_empty_vnfrs(self, mock_mongo_client):
-        self.vnfrs.find.return_value = [{"_id": "10", "vdur": []}]
-        collection_list = {"vnfrs": self.vnfrs}
-        self.mock_db.__getitem__.side_effect = collection_list.__getitem__
-        self.mock_db.list_collection_names.return_value = collection_list
-        mock_mongo_client.return_value = {"osm": self.mock_db}
-        MongoUpgrade1012.upgrade("mongo_uri")
-        self.vnfrs.update_one.assert_called_once_with({"_id": "10"}, {"$set": {"vdur": []}})
-
-    @patch("db_upgrade.MongoClient")
-    def test_update_vnfr_no_vim_info(self, mock_mongo_client):
-        vdur = {"other": {}}
-        vnfr = {"_id": "10", "vdur": [vdur]}
-        self.vnfrs.find.return_value = [vnfr]
-        collection_list = {"vnfrs": self.vnfrs}
-        self.mock_db.__getitem__.side_effect = collection_list.__getitem__
-        self.mock_db.list_collection_names.return_value = collection_list
-        mock_mongo_client.return_value = {"osm": self.mock_db}
-        MongoUpgrade1012.upgrade("mongo_uri")
-        self.assertEqual(vdur, {"other": {}})
-        self.vnfrs.update_one.assert_called_once_with({"_id": "10"}, {"$set": {"vdur": [vdur]}})
-
-    @patch("db_upgrade.MongoClient")
-    def test_update_vnfr_vim_message_not_conditions_matched(self, mock_mongo_client):
-        vim_info = {"vim_message": "HelloWorld"}
-        vim_infos = {"key1": vim_info, "key2": "value2"}
-        vdur = {"vim_info": vim_infos, "other": {}}
-        vnfr = {"_id": "10", "vdur": [vdur]}
-        self.vnfrs.find.return_value = [vnfr]
-        collection_list = {"vnfrs": self.vnfrs}
-        self.mock_db.__getitem__.side_effect = collection_list.__getitem__
-        self.mock_db.list_collection_names.return_value = collection_list
-        mock_mongo_client.return_value = {"osm": self.mock_db}
-        MongoUpgrade1012.upgrade("mongo_uri")
-        expected_vim_info = {"vim_message": "HelloWorld"}
-        self.assertEqual(vim_info, expected_vim_info)
-        self.vnfrs.update_one.assert_called_once_with({"_id": "10"}, {"$set": {"vdur": [vdur]}})
-
-    @patch("db_upgrade.MongoClient")
-    def test_update_vnfr_vim_message_is_missing(self, mock_mongo_client):
-        vim_info = {"interfaces_backup": "HelloWorld"}
-        vim_infos = {"key1": vim_info, "key2": "value2"}
-        vdur = {"vim_info": vim_infos, "other": {}}
-        vnfr = {"_id": "10", "vdur": [vdur]}
-        self.vnfrs.find.return_value = [vnfr]
-        collection_list = {"vnfrs": self.vnfrs}
-        self.mock_db.__getitem__.side_effect = collection_list.__getitem__
-        self.mock_db.list_collection_names.return_value = collection_list
-        mock_mongo_client.return_value = {"osm": self.mock_db}
-        MongoUpgrade1012.upgrade("mongo_uri")
-        expected_vim_info = {"vim_message": None, "interfaces_backup": "HelloWorld"}
-        self.assertEqual(vim_info, expected_vim_info)
-        self.vnfrs.update_one.assert_called_once_with({"_id": "10"}, {"$set": {"vdur": [vdur]}})
-
-    @patch("db_upgrade.MongoClient")
-    def test_update_vnfr_interfaces_backup_is_updated(self, mock_mongo_client):
-        vim_info = {"interfaces": "HelloWorld", "vim_message": "ByeWorld"}
-        vim_infos = {"key1": vim_info, "key2": "value2"}
-        vdur = {"vim_info": vim_infos, "other": {}}
-        vnfr = {"_id": "10", "vdur": [vdur]}
-        self.vnfrs.find.return_value = [vnfr]
-        collection_list = {"vnfrs": self.vnfrs}
-        self.mock_db.__getitem__.side_effect = collection_list.__getitem__
-        self.mock_db.list_collection_names.return_value = collection_list
-        mock_mongo_client.return_value = {"osm": self.mock_db}
-        MongoUpgrade1012.upgrade("mongo_uri")
-        expected_vim_info = {
-            "interfaces": "HelloWorld",
-            "vim_message": "ByeWorld",
-            "interfaces_backup": "HelloWorld",
-        }
-        self.assertEqual(vim_info, expected_vim_info)
-        self.vnfrs.update_one.assert_called_once_with({"_id": "10"}, {"$set": {"vdur": [vdur]}})
-
-    @patch("db_upgrade.MongoClient")
-    def test_update_k8scluster_empty_k8scluster(self, mock_mongo_client):
-        self.k8s_clusters.find.return_value = []
-        collection_list = {"k8sclusters": self.k8s_clusters}
-        self.mock_db.__getitem__.side_effect = collection_list.__getitem__
-        self.mock_db.list_collection_names.return_value = collection_list
-        mock_mongo_client.return_value = {"osm": self.mock_db}
-        MongoUpgrade1012.upgrade("mongo_uri")
-
-    @patch("db_upgrade.MongoClient")
-    def test_update_k8scluster_replace_namespace_in_helm_chart(self, mock_mongo_client):
-        helm_chart = {"id": "kube-system:Hello", "other": {}}
-        k8s_cluster = {"_id": "8", "_admin": {"helm-chart": helm_chart}}
-        self.k8s_clusters.find.return_value = [k8s_cluster]
-        collection_list = {"k8sclusters": self.k8s_clusters}
-        self.mock_db.__getitem__.side_effect = collection_list.__getitem__
-        self.mock_db.list_collection_names.return_value = collection_list
-        mock_mongo_client.return_value = {"osm": self.mock_db}
-        MongoUpgrade1012.upgrade("mongo_uri")
-        expected_helm_chart = {"id": "Hello", "other": {}}
-        expected_k8s_cluster = {"_id": "8", "_admin": {"helm-chart": expected_helm_chart}}
-        self.k8s_clusters.update_one.assert_called_once_with(
-            {"_id": "8"}, {"$set": expected_k8s_cluster}
-        )
-
-    @patch("db_upgrade.MongoClient")
-    def test_update_k8scluster_replace_namespace_in_helm_chart_v3(self, mock_mongo_client):
-        helm_chart_v3 = {"id": "kube-system:Hello", "other": {}}
-        k8s_cluster = {"_id": "8", "_admin": {"helm-chart-v3": helm_chart_v3}}
-        self.k8s_clusters.find.return_value = [k8s_cluster]
-        collection_list = {"k8sclusters": self.k8s_clusters}
-        self.mock_db.__getitem__.side_effect = collection_list.__getitem__
-        self.mock_db.list_collection_names.return_value = collection_list
-        mock_mongo_client.return_value = {"osm": self.mock_db}
-        MongoUpgrade1012.upgrade("mongo_uri")
-        expected_helm_chart_v3 = {"id": "Hello", "other": {}}
-        expected_k8s_cluster = {"_id": "8", "_admin": {"helm-chart-v3": expected_helm_chart_v3}}
-        self.k8s_clusters.update_one.assert_called_once_with(
-            {"_id": "8"}, {"$set": expected_k8s_cluster}
-        )
-
-
-class TestPatch1837(unittest.TestCase):
-    def setUp(self):
-        self.mock_db = MagicMock()
-        self.vnfrs = Mock()
-        self.nslcmops = Mock()
-
-    @patch("db_upgrade.MongoClient")
-    def test_update_vnfrs_params_no_vnfrs_or_nslcmops(self, mock_mongo_client):
-        collection_dict = {"other": {}}
-        self.mock_db.list_collection_names.return_value = collection_dict
-        mock_mongo_client.return_value = {"osm": self.mock_db}
-        MongoPatch1837.patch("mongo_uri")
-
-    @patch("db_upgrade.MongoClient")
-    def test_update_vnfrs_params_no_kdur(self, mock_mongo_client):
-        self.vnfrs.find.return_value = {"_id": "1"}
-        collection_dict = {"vnfrs": self.vnfrs, "other": {}}
-        self.mock_db.list_collection_names.return_value = collection_dict
-        mock_mongo_client.return_value = {"osm": self.mock_db}
-        MongoPatch1837.patch("mongo_uri")
-
-    @patch("db_upgrade.MongoClient")
-    def test_update_vnfrs_params_kdur_without_additional_params(self, mock_mongo_client):
-        kdur = [{"other": {}}]
-        self.vnfrs.find.return_value = [{"_id": "1", "kdur": kdur}]
-        collection_dict = {"vnfrs": self.vnfrs, "other": {}}
-        self.mock_db.list_collection_names.return_value = collection_dict
-        self.mock_db.__getitem__.side_effect = collection_dict.__getitem__
-        mock_mongo_client.return_value = {"osm": self.mock_db}
-        MongoPatch1837.patch("mongo_uri")
-        self.vnfrs.update_one.assert_called_once_with({"_id": "1"}, {"$set": {"kdur": kdur}})
-
-    @patch("db_upgrade.MongoClient")
-    def test_update_vnfrs_params_kdur_two_additional_params(self, mock_mongo_client):
-        kdur1 = {"additionalParams": "additional_params", "other": {}}
-        kdur2 = {"additionalParams": 4, "other": {}}
-        kdur = [kdur1, kdur2]
-        self.vnfrs.find.return_value = [{"_id": "1", "kdur": kdur}]
-        collection_dict = {"vnfrs": self.vnfrs, "other": {}}
-        self.mock_db.list_collection_names.return_value = collection_dict
-        self.mock_db.__getitem__.side_effect = collection_dict.__getitem__
-        mock_mongo_client.return_value = {"osm": self.mock_db}
-        MongoPatch1837.patch("mongo_uri")
-        self.vnfrs.update_one.assert_called_once_with(
-            {"_id": "1"}, {"$set": {"kdur": [kdur1, {"additionalParams": "4", "other": {}}]}}
-        )
-
-    @patch("db_upgrade.MongoClient")
-    def test_update_nslcmops_params_no_nslcmops(self, mock_mongo_client):
-        self.nslcmops.find.return_value = []
-        collection_dict = {"nslcmops": self.nslcmops, "other": {}}
-        self.mock_db.list_collection_names.return_value = collection_dict
-        self.mock_db.__getitem__.side_effect = collection_dict.__getitem__
-        mock_mongo_client.return_value = {"osm": self.mock_db}
-        MongoPatch1837.patch("mongo_uri")
-
-    @patch("db_upgrade.MongoClient")
-    def test_update_nslcmops_additional_params(self, mock_mongo_client):
-        operation_params_list = {"additionalParamsForVnf": [1, 2, 3]}
-        operation_params_dict = {"primitive_params": {"dict_key": 5}}
-        nslcmops1 = {"_id": "1", "other": {}}
-        nslcmops2 = {"_id": "2", "operationParams": operation_params_list, "other": {}}
-        nslcmops3 = {"_id": "3", "operationParams": operation_params_dict, "other": {}}
-        self.nslcmops.find.return_value = [nslcmops1, nslcmops2, nslcmops3]
-        collection_dict = {"nslcmops": self.nslcmops, "other": {}}
-        self.mock_db.list_collection_names.return_value = collection_dict
-        self.mock_db.__getitem__.side_effect = collection_dict.__getitem__
-        mock_mongo_client.return_value = {"osm": self.mock_db}
-        MongoPatch1837.patch("mongo_uri")
-        call1 = call(
-            {"_id": "2"}, {"$set": {"operationParams": {"additionalParamsForVnf": "[1, 2, 3]"}}}
-        )
-        call2 = call(
-            {"_id": "3"}, {"$set": {"operationParams": {"primitive_params": '{"dict_key": 5}'}}}
-        )
-        expected_calls = [call1, call2]
-        self.nslcmops.update_one.assert_has_calls(expected_calls)
-
-
-class TestMongoUpgrade(unittest.TestCase):
-    def setUp(self):
-        self.mongo = MongoUpgrade("http://fake_mongo:27017")
-        self.upgrade_function = Mock()
-        self.patch_function = Mock()
-        db_upgrade.MONGODB_UPGRADE_FUNCTIONS = {"9": {"10": [self.upgrade_function]}}
-        db_upgrade.BUG_FIXES = {1837: self.patch_function}
-
-    def test_validate_upgrade_fail_target(self):
-        valid_current = "9"
-        invalid_target = "7"
-        with self.assertRaises(Exception) as context:
-            self.mongo._validate_upgrade(valid_current, invalid_target)
-        self.assertEqual("cannot upgrade from version 9 to 7.", str(context.exception))
-
-    def test_validate_upgrade_fail_current(self):
-        invalid_current = "7"
-        invalid_target = "8"
-        with self.assertRaises(Exception) as context:
-            self.mongo._validate_upgrade(invalid_current, invalid_target)
-        self.assertEqual("cannot upgrade from 7 version.", str(context.exception))
-
-    def test_validate_upgrade_pass(self):
-        valid_current = "9"
-        valid_target = "10"
-        self.assertIsNone(self.mongo._validate_upgrade(valid_current, valid_target))
-
-    @patch("db_upgrade.MongoUpgrade._validate_upgrade")
-    def test_update_mongo_success(self, mock_validate):
-        valid_current = "9"
-        valid_target = "10"
-        mock_validate.return_value = ""
-        self.mongo.upgrade(valid_current, valid_target)
-        self.upgrade_function.assert_called_once()
-
-    def test_validate_apply_patch(self):
-        bug_number = 1837
-        self.mongo.apply_patch(bug_number)
-        self.patch_function.assert_called_once()
-
-    def test_validate_apply_patch_invalid_bug_fail(self):
-        bug_number = 2
-        with self.assertRaises(Exception) as context:
-            self.mongo.apply_patch(bug_number)
-        self.assertEqual("There is no patch for bug 2", str(context.exception))
-        self.patch_function.assert_not_called()
-
-
-class TestMysqlUpgrade(unittest.TestCase):
-    def setUp(self):
-        self.mysql = MysqlUpgrade("mysql://fake_mysql:23023")
-        self.upgrade_function = Mock()
-        db_upgrade.MYSQL_UPGRADE_FUNCTIONS = {"9": {"10": [self.upgrade_function]}}
-
-    def test_validate_upgrade_mysql_fail_current(self):
-        invalid_current = "7"
-        invalid_target = "8"
-        with self.assertRaises(Exception) as context:
-            self.mysql._validate_upgrade(invalid_current, invalid_target)
-        self.assertEqual("cannot upgrade from 7 version.", str(context.exception))
-
-    def test_validate_upgrade_mysql_fail_target(self):
-        valid_current = "9"
-        invalid_target = "7"
-        with self.assertRaises(Exception) as context:
-            self.mysql._validate_upgrade(valid_current, invalid_target)
-        self.assertEqual("cannot upgrade from version 9 to 7.", str(context.exception))
-
-    def test_validate_upgrade_mysql_success(self):
-        valid_current = "9"
-        valid_target = "10"
-        self.assertIsNone(self.mysql._validate_upgrade(valid_current, valid_target))
-
-    @patch("db_upgrade.MysqlUpgrade._validate_upgrade")
-    def test_upgrade_mysql_success(self, mock_validate):
-        valid_current = "9"
-        valid_target = "10"
-        mock_validate.return_value = ""
-        self.mysql.upgrade(valid_current, valid_target)
-        self.upgrade_function.assert_called_once()
diff --git a/installers/charm/osm-update-db-operator/tox.ini b/installers/charm/osm-update-db-operator/tox.ini
deleted file mode 100644 (file)
index bcf628a..0000000
+++ /dev/null
@@ -1,104 +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.
-
-[tox]
-skipsdist=True
-skip_missing_interpreters = True
-envlist = lint, unit
-
-[vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-;lib_path = {toxinidir}/lib/charms/
-all_path = {[vars]src_path} {[vars]tst_path}
-
-[testenv]
-basepython = python3
-setenv =
-  PYTHONPATH = {toxinidir}:{toxinidir}/lib:{[vars]src_path}
-  PYTHONBREAKPOINT=ipdb.set_trace
-passenv =
-  PYTHONPATH
-  HOME
-  PATH
-  CHARM_BUILD_DIR
-  MODEL_SETTINGS
-  HTTP_PROXY
-  HTTPS_PROXY
-  NO_PROXY
-
-[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>= 4.0.0, < 5.0.0
-    flake8-docstrings
-    flake8-copyright
-    flake8-builtins
-    # prospector[with_everything]
-    pylint
-    pyproject-flake8
-    pep8-naming
-    isort
-    codespell
-    yamllint
-    -r{toxinidir}/requirements.txt
-commands =
-    codespell {toxinidir}/*.yaml {toxinidir}/*.ini {toxinidir}/*.md \
-      {toxinidir}/*.toml {toxinidir}/*.txt {toxinidir}/.github
-    # prospector -A -F -T
-    pylint -E {[vars]src_path}
-    yamllint -d '\{extends: default, ignore: "build\n.tox" \}' .
-    # 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
-    pytest-cov
-    coverage[toml]
-    -r{toxinidir}/requirements.txt
-commands =
-    pytest --ignore={[vars]tst_path}integration --cov={[vars]src_path} --cov-report=xml
-    coverage report
-
-[testenv:security]
-description = Run security tests
-deps =
-    bandit
-    safety
-commands =
-    bandit -r {[vars]src_path}
-    - safety check
-
-[testenv:integration]
-description = Run integration tests
-deps =
-    pytest
-    pytest-operator
-commands =
-    pytest -v --tb native --ignore={[vars]tst_path}unit --log-cli-level=INFO -s {posargs}
diff --git a/installers/charm/prometheus/.gitignore b/installers/charm/prometheus/.gitignore
deleted file mode 100644 (file)
index 2885df2..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-venv
-.vscode
-build
-*.charm
-.coverage
-coverage.xml
-.stestr
-cover
-release
\ No newline at end of file
diff --git a/installers/charm/prometheus/.jujuignore b/installers/charm/prometheus/.jujuignore
deleted file mode 100644 (file)
index 3ae3e7d..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-venv
-.vscode
-build
-*.charm
-.coverage
-coverage.xml
-.gitignore
-.stestr
-cover
-release
-tests/
-requirements*
-tox.ini
diff --git a/installers/charm/prometheus/.yamllint.yaml b/installers/charm/prometheus/.yamllint.yaml
deleted file mode 100644 (file)
index d71fb69..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
----
-extends: default
-
-yaml-files:
-  - "*.yaml"
-  - "*.yml"
-  - ".yamllint"
-ignore: |
-  .tox
-  cover/
-  build/
-  venv
-  release/
diff --git a/installers/charm/prometheus/README.md b/installers/charm/prometheus/README.md
deleted file mode 100644 (file)
index 0486c0d..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<!-- 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.
-
-For those usages not covered by the Apache License, Version 2.0 please
-contact: legal@canonical.com
-
-To get in touch with the maintainers, please contact:
-osm-charmers@lists.launchpad.net -->
-
-# Prometheus operator Charm for Kubernetes
-
-## Requirements
diff --git a/installers/charm/prometheus/actions.yaml b/installers/charm/prometheus/actions.yaml
deleted file mode 100644 (file)
index e41f3df..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-backup:
-  description: "Do a mongodb backup"
diff --git a/installers/charm/prometheus/charmcraft.yaml b/installers/charm/prometheus/charmcraft.yaml
deleted file mode 100644 (file)
index 87d0463..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-type: charm
-bases:
-  - build-on:
-      - name: ubuntu
-        channel: "20.04"
-        architectures: ["amd64"]
-    run-on:
-      - name: ubuntu
-        channel: "20.04"
-        architectures:
-          - amd64
-          - aarch64
-          - arm64
-parts:
-  charm:
-    build-packages:
-      - cargo
-      - git
-      - libffi-dev
-      - rustc
diff --git a/installers/charm/prometheus/config.yaml b/installers/charm/prometheus/config.yaml
deleted file mode 100644 (file)
index b25eaba..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-options:
-  web-subpath:
-    description: Subpath for accessing Prometheus
-    type: string
-    default: /
-  default-target:
-    description: Default target to be added in Prometheus
-    type: string
-    default: ""
-  max_file_size:
-    type: int
-    description: |
-      The maximum file size, in megabytes. If there is a reverse proxy in front
-      of Keystone, it may need to be configured to handle the requested size.
-      Note: if set to 0, there is no limit.
-    default: 0
-  ingress_class:
-    type: string
-    description: |
-      Ingress class name. This is useful for selecting the ingress to be used
-      in case there are multiple ingresses in the underlying k8s clusters.
-  ingress_whitelist_source_range:
-    type: string
-    description: |
-      A comma-separated list of CIDRs to store in the
-      ingress.kubernetes.io/whitelist-source-range annotation.
-
-      This can be used to lock down access to
-      Keystone based on source IP address.
-    default: ""
-  tls_secret_name:
-    type: string
-    description: TLS Secret name
-    default: ""
-  site_url:
-    type: string
-    description: Ingress URL
-    default: ""
-  cluster_issuer:
-    type: string
-    description: Name of the cluster issuer for TLS certificates
-    default: ""
-  enable_web_admin_api:
-    type: boolean
-    description: Boolean to enable the web admin api
-    default: false
-  image_pull_policy:
-    type: string
-    description: |
-      ImagePullPolicy configuration for the pod.
-      Possible values: always, ifnotpresent, never
-    default: always
-  security_context:
-    description: Enables the security context of the pods
-    type: boolean
-    default: false
-  web_config_username:
-    type: string
-    default: admin
-    description: Username to access the Prometheus Web Interface
-  web_config_password:
-    type: string
-    default: admin
-    description: Password to access the Prometheus Web Interface
diff --git a/installers/charm/prometheus/icon.svg b/installers/charm/prometheus/icon.svg
deleted file mode 100644 (file)
index 5c51f66..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
-
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   version="1.1"
-   id="Layer_1"
-   x="0px"
-   y="0px"
-   width="115.333px"
-   height="114px"
-   viewBox="0 0 115.333 114"
-   enable-background="new 0 0 115.333 114"
-   xml:space="preserve"
-   sodipodi:docname="prometheus_logo_orange.svg"
-   inkscape:version="0.92.1 r15371"><metadata
-     id="metadata4495"><rdf:RDF><cc:Work
-         rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
-     id="defs4493" /><sodipodi:namedview
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1"
-     objecttolerance="10"
-     gridtolerance="10"
-     guidetolerance="10"
-     inkscape:pageopacity="0"
-     inkscape:pageshadow="2"
-     inkscape:window-width="1484"
-     inkscape:window-height="886"
-     id="namedview4491"
-     showgrid="false"
-     inkscape:zoom="5.2784901"
-     inkscape:cx="60.603667"
-     inkscape:cy="60.329656"
-     inkscape:window-x="54"
-     inkscape:window-y="7"
-     inkscape:window-maximized="0"
-     inkscape:current-layer="Layer_1" /><g
-     id="Layer_2" /><path
-     style="fill:#e6522c;fill-opacity:1"
-     inkscape:connector-curvature="0"
-     id="path4486"
-     d="M 56.667,0.667 C 25.372,0.667 0,26.036 0,57.332 c 0,31.295 25.372,56.666 56.667,56.666 31.295,0 56.666,-25.371 56.666,-56.666 0,-31.296 -25.372,-56.665 -56.666,-56.665 z m 0,106.055 c -8.904,0 -16.123,-5.948 -16.123,-13.283 H 72.79 c 0,7.334 -7.219,13.283 -16.123,13.283 z M 83.297,89.04 H 30.034 V 79.382 H 83.298 V 89.04 Z M 83.106,74.411 H 30.186 C 30.01,74.208 29.83,74.008 29.66,73.802 24.208,67.182 22.924,63.726 21.677,60.204 c -0.021,-0.116 6.611,1.355 11.314,2.413 0,0 2.42,0.56 5.958,1.205 -3.397,-3.982 -5.414,-9.044 -5.414,-14.218 0,-11.359 8.712,-21.285 5.569,-29.308 3.059,0.249 6.331,6.456 6.552,16.161 3.252,-4.494 4.613,-12.701 4.613,-17.733 0,-5.21 3.433,-11.262 6.867,-11.469 -3.061,5.045 0.793,9.37 4.219,20.099 1.285,4.03 1.121,10.812 2.113,15.113 C 63.797,33.534 65.333,20.5 71,16 c -2.5,5.667 0.37,12.758 2.333,16.167 3.167,5.5 5.087,9.667 5.087,17.548 0,5.284 -1.951,10.259 -5.242,14.148 3.742,-0.702 6.326,-1.335 6.326,-1.335 l 12.152,-2.371 c 10e-4,-10e-4 -1.765,7.261 -8.55,14.254 z" /></svg>
\ No newline at end of file
diff --git a/installers/charm/prometheus/metadata.yaml b/installers/charm/prometheus/metadata.yaml
deleted file mode 100644 (file)
index 932ccc2..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-name: osm-prometheus
-summary: OSM Prometheus
-description: |
-  A CAAS charm to deploy OSM's Prometheus.
-series:
-  - kubernetes
-tags:
-  - kubernetes
-  - osm
-  - prometheus
-min-juju-version: 2.8.0
-deployment:
-  type: stateful
-  service: cluster
-resources:
-  backup-image:
-    type: oci-image
-    description: Container image to run backup actions
-    upstream-source: "ed1000/prometheus-backup:latest"
-  image:
-    type: oci-image
-    description: OSM docker image for Prometheus
-    upstream-source: "ubuntu/prometheus:latest"
-provides:
-  prometheus:
-    interface: prometheus
-storage:
-  data:
-    type: filesystem
-    location: /prometheus
diff --git a/installers/charm/prometheus/requirements-test.txt b/installers/charm/prometheus/requirements-test.txt
deleted file mode 100644 (file)
index cf61dd4..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-mock==4.0.3
diff --git a/installers/charm/prometheus/requirements.txt b/installers/charm/prometheus/requirements.txt
deleted file mode 100644 (file)
index db13e51..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-git+https://github.com/charmed-osm/ops-lib-charmed-osm/@master
-requests
-urllib3>1.25.9
-bcrypt
diff --git a/installers/charm/prometheus/src/charm.py b/installers/charm/prometheus/src/charm.py
deleted file mode 100755 (executable)
index af39a13..0000000
+++ /dev/null
@@ -1,298 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-# pylint: disable=E0213
-
-import base64
-from ipaddress import ip_network
-import logging
-from typing import NoReturn, Optional
-from urllib.parse import urlparse
-
-import bcrypt
-from oci_image import OCIImageResource
-from ops.framework import EventBase
-from ops.main import main
-from opslib.osm.charm import CharmedOsmBase
-from opslib.osm.interfaces.prometheus import PrometheusServer
-from opslib.osm.pod import (
-    ContainerV3Builder,
-    FilesV3Builder,
-    IngressResourceV3Builder,
-    PodSpecV3Builder,
-)
-from opslib.osm.validator import (
-    ModelValidator,
-    validator,
-)
-import requests
-
-
-logger = logging.getLogger(__name__)
-
-PORT = 9090
-
-
-class ConfigModel(ModelValidator):
-    web_subpath: str
-    default_target: str
-    max_file_size: int
-    site_url: Optional[str]
-    cluster_issuer: Optional[str]
-    ingress_class: Optional[str]
-    ingress_whitelist_source_range: Optional[str]
-    tls_secret_name: Optional[str]
-    enable_web_admin_api: bool
-    image_pull_policy: str
-    security_context: bool
-    web_config_username: str
-    web_config_password: str
-
-    @validator("web_subpath")
-    def validate_web_subpath(cls, v):
-        if len(v) < 1:
-            raise ValueError("web-subpath must be a non-empty string")
-        return v
-
-    @validator("max_file_size")
-    def validate_max_file_size(cls, v):
-        if v < 0:
-            raise ValueError("value must be equal or greater than 0")
-        return v
-
-    @validator("site_url")
-    def validate_site_url(cls, v):
-        if v:
-            parsed = urlparse(v)
-            if not parsed.scheme.startswith("http"):
-                raise ValueError("value must start with http")
-        return v
-
-    @validator("ingress_whitelist_source_range")
-    def validate_ingress_whitelist_source_range(cls, v):
-        if v:
-            ip_network(v)
-        return v
-
-    @validator("image_pull_policy")
-    def validate_image_pull_policy(cls, v):
-        values = {
-            "always": "Always",
-            "ifnotpresent": "IfNotPresent",
-            "never": "Never",
-        }
-        v = v.lower()
-        if v not in values.keys():
-            raise ValueError("value must be always, ifnotpresent or never")
-        return values[v]
-
-
-class PrometheusCharm(CharmedOsmBase):
-
-    """Prometheus Charm."""
-
-    def __init__(self, *args) -> NoReturn:
-        """Prometheus Charm constructor."""
-        super().__init__(*args, oci_image="image")
-
-        # Registering provided relation events
-        self.prometheus = PrometheusServer(self, "prometheus")
-        self.framework.observe(
-            self.on.prometheus_relation_joined,  # pylint: disable=E1101
-            self._publish_prometheus_info,
-        )
-
-        # Registering actions
-        self.framework.observe(
-            self.on.backup_action,  # pylint: disable=E1101
-            self._on_backup_action,
-        )
-
-    def _publish_prometheus_info(self, event: EventBase) -> NoReturn:
-        config = ConfigModel(**dict(self.config))
-        self.prometheus.publish_info(
-            self.app.name,
-            PORT,
-            user=config.web_config_username,
-            password=config.web_config_password,
-        )
-
-    def _on_backup_action(self, event: EventBase) -> NoReturn:
-        url = f"http://{self.model.app.name}:{PORT}/api/v1/admin/tsdb/snapshot"
-        result = requests.post(url)
-
-        if result.status_code == 200:
-            event.set_results({"backup-name": result.json()["name"]})
-        else:
-            event.fail(f"status-code: {result.status_code}")
-
-    def _build_config_file(self, config: ConfigModel):
-        files_builder = FilesV3Builder()
-        files_builder.add_file(
-            "prometheus.yml",
-            (
-                "global:\n"
-                "  scrape_interval: 15s\n"
-                "  evaluation_interval: 15s\n"
-                "alerting:\n"
-                "  alertmanagers:\n"
-                "    - static_configs:\n"
-                "        - targets:\n"
-                "rule_files:\n"
-                "scrape_configs:\n"
-                "  - job_name: 'prometheus'\n"
-                "    static_configs:\n"
-                f"      - targets: [{config.default_target}]\n"
-            ),
-        )
-        return files_builder.build()
-
-    def _build_webconfig_file(self):
-        files_builder = FilesV3Builder()
-        files_builder.add_file("web.yml", "web-config-file", secret=True)
-        return files_builder.build()
-
-    def build_pod_spec(self, image_info):
-        # Validate config
-        config = ConfigModel(**dict(self.config))
-        # Create Builder for the PodSpec
-        pod_spec_builder = PodSpecV3Builder(
-            enable_security_context=config.security_context
-        )
-
-        # Build Backup Container
-        backup_image = OCIImageResource(self, "backup-image")
-        backup_image_info = backup_image.fetch()
-        backup_container_builder = ContainerV3Builder("prom-backup", backup_image_info)
-        backup_container = backup_container_builder.build()
-
-        # Add backup container to pod spec
-        pod_spec_builder.add_container(backup_container)
-
-        # Add pod secrets
-        prometheus_secret_name = f"{self.app.name}-secret"
-        pod_spec_builder.add_secret(
-            prometheus_secret_name,
-            {
-                "web-config-file": (
-                    "basic_auth_users:\n"
-                    f"  {config.web_config_username}: {self._hash_password(config.web_config_password)}\n"
-                )
-            },
-        )
-
-        # Build Container
-        container_builder = ContainerV3Builder(
-            self.app.name,
-            image_info,
-            config.image_pull_policy,
-            run_as_non_root=config.security_context,
-        )
-        container_builder.add_port(name=self.app.name, port=PORT)
-        token = self._base64_encode(
-            f"{config.web_config_username}:{config.web_config_password}"
-        )
-        container_builder.add_http_readiness_probe(
-            "/-/ready",
-            PORT,
-            initial_delay_seconds=10,
-            timeout_seconds=30,
-            http_headers=[("Authorization", f"Basic {token}")],
-        )
-        container_builder.add_http_liveness_probe(
-            "/-/healthy",
-            PORT,
-            initial_delay_seconds=30,
-            period_seconds=30,
-            http_headers=[("Authorization", f"Basic {token}")],
-        )
-        command = [
-            "/bin/prometheus",
-            "--config.file=/etc/prometheus/prometheus.yml",
-            "--web.config.file=/etc/prometheus/web-config/web.yml",
-            "--storage.tsdb.path=/prometheus",
-            "--web.console.libraries=/usr/share/prometheus/console_libraries",
-            "--web.console.templates=/usr/share/prometheus/consoles",
-            f"--web.route-prefix={config.web_subpath}",
-            f"--web.external-url=http://localhost:{PORT}{config.web_subpath}",
-        ]
-        if config.enable_web_admin_api:
-            command.append("--web.enable-admin-api")
-        container_builder.add_command(command)
-        container_builder.add_volume_config(
-            "config", "/etc/prometheus", self._build_config_file(config)
-        )
-        container_builder.add_volume_config(
-            "web-config",
-            "/etc/prometheus/web-config",
-            self._build_webconfig_file(),
-            secret_name=prometheus_secret_name,
-        )
-        container = container_builder.build()
-        # Add container to pod spec
-        pod_spec_builder.add_container(container)
-        # Add ingress resources to pod spec if site url exists
-        if config.site_url:
-            parsed = urlparse(config.site_url)
-            annotations = {
-                "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
-                    str(config.max_file_size) + "m"
-                    if config.max_file_size > 0
-                    else config.max_file_size
-                )
-            }
-            if config.ingress_class:
-                annotations["kubernetes.io/ingress.class"] = config.ingress_class
-            ingress_resource_builder = IngressResourceV3Builder(
-                f"{self.app.name}-ingress", annotations
-            )
-
-            if config.ingress_whitelist_source_range:
-                annotations[
-                    "nginx.ingress.kubernetes.io/whitelist-source-range"
-                ] = config.ingress_whitelist_source_range
-
-            if config.cluster_issuer:
-                annotations["cert-manager.io/cluster-issuer"] = config.cluster_issuer
-
-            if parsed.scheme == "https":
-                ingress_resource_builder.add_tls(
-                    [parsed.hostname], config.tls_secret_name
-                )
-            else:
-                annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
-
-            ingress_resource_builder.add_rule(parsed.hostname, self.app.name, PORT)
-            ingress_resource = ingress_resource_builder.build()
-            pod_spec_builder.add_ingress_resource(ingress_resource)
-        return pod_spec_builder.build()
-
-    def _hash_password(self, password):
-        hashed_password = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
-        return hashed_password.decode()
-
-    def _base64_encode(self, phrase: str) -> str:
-        return base64.b64encode(phrase.encode("utf-8")).decode("utf-8")
-
-
-if __name__ == "__main__":
-    main(PrometheusCharm)
diff --git a/installers/charm/prometheus/src/pod_spec.py b/installers/charm/prometheus/src/pod_spec.py
deleted file mode 100644 (file)
index 202114e..0000000
+++ /dev/null
@@ -1,380 +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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-from ipaddress import ip_network
-import logging
-from typing import Any, Dict, List
-from urllib.parse import urlparse
-
-logger = logging.getLogger(__name__)
-
-
-def _validate_max_file_size(max_file_size: int, site_url: str) -> bool:
-    """Validate max_file_size.
-
-    Args:
-        max_file_size (int): maximum file size allowed.
-        site_url (str): endpoint url.
-
-    Returns:
-        bool: True if valid, false otherwise.
-    """
-    if not site_url:
-        return True
-
-    parsed = urlparse(site_url)
-
-    if not parsed.scheme.startswith("http"):
-        return True
-
-    if max_file_size is None:
-        return False
-
-    return max_file_size >= 0
-
-
-def _validate_ip_network(network: str) -> bool:
-    """Validate IP network.
-
-    Args:
-        network (str): IP network range.
-
-    Returns:
-        bool: True if valid, false otherwise.
-    """
-    if not network:
-        return True
-
-    try:
-        ip_network(network)
-    except ValueError:
-        return False
-
-    return True
-
-
-def _validate_data(config_data: Dict[str, Any], relation_data: Dict[str, Any]) -> bool:
-    """Validates passed information.
-
-    Args:
-        config_data (Dict[str, Any]): configuration information.
-        relation_data (Dict[str, Any]): relation information
-
-    Raises:
-        ValueError: when config and/or relation data is not valid.
-    """
-    config_validators = {
-        "web_subpath": lambda value, _: isinstance(value, str) and len(value) > 0,
-        "default_target": lambda value, _: isinstance(value, str),
-        "site_url": lambda value, _: isinstance(value, str)
-        if value is not None
-        else True,
-        "max_file_size": lambda value, values: _validate_max_file_size(
-            value, values.get("site_url")
-        ),
-        "ingress_whitelist_source_range": lambda value, _: _validate_ip_network(value),
-        "tls_secret_name": lambda value, _: isinstance(value, str)
-        if value is not None
-        else True,
-        "enable_web_admin_api": lambda value, _: isinstance(value, bool),
-    }
-    relation_validators = {}
-    problems = []
-
-    for key, validator in config_validators.items():
-        valid = validator(config_data.get(key), config_data)
-
-        if not valid:
-            problems.append(key)
-
-    for key, validator in relation_validators.items():
-        valid = validator(relation_data.get(key), relation_data)
-
-        if not valid:
-            problems.append(key)
-
-    if len(problems) > 0:
-        raise ValueError("Errors found in: {}".format(", ".join(problems)))
-
-    return True
-
-
-def _make_pod_ports(port: int) -> List[Dict[str, Any]]:
-    """Generate pod ports details.
-
-    Args:
-        port (int): port to expose.
-
-    Returns:
-        List[Dict[str, Any]]: pod port details.
-    """
-    return [{"name": "prometheus", "containerPort": port, "protocol": "TCP"}]
-
-
-def _make_pod_envconfig(
-    config: Dict[str, Any], relation_state: Dict[str, Any]
-) -> Dict[str, Any]:
-    """Generate pod environment configuration.
-
-    Args:
-        config (Dict[str, Any]): configuration information.
-        relation_state (Dict[str, Any]): relation state information.
-
-    Returns:
-        Dict[str, Any]: pod environment configuration.
-    """
-    envconfig = {}
-
-    return envconfig
-
-
-def _make_pod_ingress_resources(
-    config: Dict[str, Any], app_name: str, port: int
-) -> List[Dict[str, Any]]:
-    """Generate pod ingress resources.
-
-    Args:
-        config (Dict[str, Any]): configuration information.
-        app_name (str): application name.
-        port (int): port to expose.
-
-    Returns:
-        List[Dict[str, Any]]: pod ingress resources.
-    """
-    site_url = config.get("site_url")
-
-    if not site_url:
-        return
-
-    parsed = urlparse(site_url)
-
-    if not parsed.scheme.startswith("http"):
-        return
-
-    max_file_size = config["max_file_size"]
-    ingress_whitelist_source_range = config["ingress_whitelist_source_range"]
-
-    annotations = {
-        "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
-            str(max_file_size) + "m" if max_file_size > 0 else max_file_size
-        ),
-    }
-
-    if ingress_whitelist_source_range:
-        annotations[
-            "nginx.ingress.kubernetes.io/whitelist-source-range"
-        ] = ingress_whitelist_source_range
-
-    ingress_spec_tls = None
-
-    if parsed.scheme == "https":
-        ingress_spec_tls = [{"hosts": [parsed.hostname]}]
-        tls_secret_name = config["tls_secret_name"]
-        if tls_secret_name:
-            ingress_spec_tls[0]["secretName"] = tls_secret_name
-    else:
-        annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
-
-    ingress = {
-        "name": "{}-ingress".format(app_name),
-        "annotations": annotations,
-        "spec": {
-            "rules": [
-                {
-                    "host": parsed.hostname,
-                    "http": {
-                        "paths": [
-                            {
-                                "path": "/",
-                                "backend": {
-                                    "serviceName": app_name,
-                                    "servicePort": port,
-                                },
-                            }
-                        ]
-                    },
-                }
-            ]
-        },
-    }
-    if ingress_spec_tls:
-        ingress["spec"]["tls"] = ingress_spec_tls
-
-    return [ingress]
-
-
-def _make_pod_files(config: Dict[str, Any]) -> List[Dict[str, Any]]:
-    """Generating ConfigMap information
-
-    Args:
-        config (Dict[str, Any]): configuration information.
-
-    Returns:
-        List[Dict[str, Any]]: ConfigMap information.
-    """
-    files = [
-        {
-            "name": "config",
-            "mountPath": "/etc/prometheus",
-            "files": [
-                {
-                    "path": "prometheus.yml",
-                    "content": (
-                        "global:\n"
-                        "  scrape_interval: 15s\n"
-                        "  evaluation_interval: 15s\n"
-                        "alerting:\n"
-                        "  alertmanagers:\n"
-                        "    - static_configs:\n"
-                        "        - targets:\n"
-                        "rule_files:\n"
-                        "scrape_configs:\n"
-                        "  - job_name: 'prometheus'\n"
-                        "    static_configs:\n"
-                        "      - targets: [{}]\n".format(config["default_target"])
-                    ),
-                }
-            ],
-        }
-    ]
-
-    return files
-
-
-def _make_readiness_probe(port: int) -> Dict[str, Any]:
-    """Generate readiness probe.
-
-    Args:
-        port (int): service port.
-
-    Returns:
-        Dict[str, Any]: readiness probe.
-    """
-    return {
-        "httpGet": {
-            "path": "/-/ready",
-            "port": port,
-        },
-        "initialDelaySeconds": 10,
-        "timeoutSeconds": 30,
-    }
-
-
-def _make_liveness_probe(port: int) -> Dict[str, Any]:
-    """Generate liveness probe.
-
-    Args:
-        port (int): service port.
-
-    Returns:
-        Dict[str, Any]: liveness probe.
-    """
-    return {
-        "httpGet": {
-            "path": "/-/healthy",
-            "port": port,
-        },
-        "initialDelaySeconds": 30,
-        "periodSeconds": 30,
-    }
-
-
-def _make_pod_command(config: Dict[str, Any], port: int) -> List[str]:
-    """Generate the startup command.
-
-    Args:
-        config (Dict[str, Any]): Configuration information.
-        port (int): port.
-
-    Returns:
-        List[str]: command to startup the process.
-    """
-    command = [
-        "/bin/prometheus",
-        "--config.file=/etc/prometheus/prometheus.yml",
-        "--storage.tsdb.path=/prometheus",
-        "--web.console.libraries=/usr/share/prometheus/console_libraries",
-        "--web.console.templates=/usr/share/prometheus/consoles",
-        "--web.route-prefix={}".format(config.get("web_subpath")),
-        "--web.external-url=http://localhost:{}{}".format(
-            port, config.get("web_subpath")
-        ),
-    ]
-    if config.get("enable_web_admin_api"):
-        command.append("--web.enable-admin-api")
-    return command
-
-
-def make_pod_spec(
-    image_info: Dict[str, str],
-    config: Dict[str, Any],
-    relation_state: Dict[str, Any],
-    app_name: str = "prometheus",
-    port: int = 9090,
-) -> Dict[str, Any]:
-    """Generate the pod spec information.
-
-    Args:
-        image_info (Dict[str, str]): Object provided by
-            OCIImageResource("image").fetch().
-        config (Dict[str, Any]): Configuration information.
-        relation_state (Dict[str, Any]): Relation state information.
-        app_name (str, optional): Application name. Defaults to "ro".
-        port (int, optional): Port for the container. Defaults to 9090.
-
-    Returns:
-        Dict[str, Any]: Pod spec dictionary for the charm.
-    """
-    if not image_info:
-        return None
-
-    _validate_data(config, relation_state)
-
-    ports = _make_pod_ports(port)
-    env_config = _make_pod_envconfig(config, relation_state)
-    files = _make_pod_files(config)
-    readiness_probe = _make_readiness_probe(port)
-    liveness_probe = _make_liveness_probe(port)
-    ingress_resources = _make_pod_ingress_resources(config, app_name, port)
-    command = _make_pod_command(config, port)
-
-    return {
-        "version": 3,
-        "containers": [
-            {
-                "name": app_name,
-                "imageDetails": image_info,
-                "imagePullPolicy": "Always",
-                "ports": ports,
-                "envConfig": env_config,
-                "volumeConfig": files,
-                "command": command,
-                "kubernetes": {
-                    "readinessProbe": readiness_probe,
-                    "livenessProbe": liveness_probe,
-                },
-            }
-        ],
-        "kubernetesResources": {
-            "ingressResources": ingress_resources or [],
-        },
-    }
diff --git a/installers/charm/prometheus/tests/__init__.py b/installers/charm/prometheus/tests/__init__.py
deleted file mode 100644 (file)
index 446d5ce..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2020 Canonical Ltd.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-#         http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-"""Init mocking for unit tests."""
-
-import sys
-
-
-import mock
-
-
-class OCIImageResourceErrorMock(Exception):
-    pass
-
-
-sys.path.append("src")
-
-oci_image = mock.MagicMock()
-oci_image.OCIImageResourceError = OCIImageResourceErrorMock
-sys.modules["oci_image"] = oci_image
-sys.modules["oci_image"].OCIImageResource().fetch.return_value = {}
diff --git a/installers/charm/prometheus/tests/test_charm.py b/installers/charm/prometheus/tests/test_charm.py
deleted file mode 100644 (file)
index 965400a..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2020 Canonical Ltd.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-#         http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-import sys
-from typing import NoReturn
-import unittest
-
-from charm import PrometheusCharm
-from ops.model import ActiveStatus
-from ops.testing import Harness
-
-
-class TestCharm(unittest.TestCase):
-    """Prometheus Charm unit tests."""
-
-    def setUp(self) -> NoReturn:
-        """Test setup"""
-        self.image_info = sys.modules["oci_image"].OCIImageResource().fetch()
-        self.harness = Harness(PrometheusCharm)
-        self.harness.set_leader(is_leader=True)
-        self.harness.begin()
-        self.config = {
-            "web-subpath": "/",
-            "default-target": "",
-            "max_file_size": 0,
-            "ingress_whitelist_source_range": "",
-            "tls_secret_name": "",
-            "site_url": "https://prometheus.192.168.100.100.nip.io",
-            "cluster_issuer": "vault-issuer",
-            "enable_web_admin_api": False,
-            "web_config_username": "admin",
-            "web_config_password": "1234",
-        }
-        self.harness.update_config(self.config)
-
-    def test_config_changed(
-        self,
-    ) -> NoReturn:
-        """Test ingress resources without HTTP."""
-
-        self.harness.charm.on.config_changed.emit()
-
-        # Assertions
-        self.assertIsInstance(self.harness.charm.unit.status, ActiveStatus)
-
-    def test_config_changed_non_leader(
-        self,
-    ) -> NoReturn:
-        """Test ingress resources without HTTP."""
-        self.harness.set_leader(is_leader=False)
-        self.harness.charm.on.config_changed.emit()
-
-        # Assertions
-        self.assertIsInstance(self.harness.charm.unit.status, ActiveStatus)
-
-    def test_publish_prometheus_info(
-        self,
-    ) -> NoReturn:
-        """Test to see if prometheus relation is updated."""
-        expected_result = {
-            "hostname": self.harness.charm.app.name,
-            "port": "9090",
-            "user": "admin",
-            "password": "1234",
-        }
-
-        relation_id = self.harness.add_relation("prometheus", "mon")
-        self.harness.add_relation_unit(relation_id, "mon/0")
-        relation_data = self.harness.get_relation_data(
-            relation_id, self.harness.charm.app.name
-        )
-
-        self.assertDictEqual(expected_result, relation_data)
-
-    def test_publish_prometheus_info_non_leader(
-        self,
-    ) -> NoReturn:
-        """Test to see if prometheus relation is updated."""
-        expected_result = {}
-
-        self.harness.set_leader(is_leader=False)
-        relation_id = self.harness.add_relation("prometheus", "mon")
-        self.harness.add_relation_unit(relation_id, "mon/0")
-        relation_data = self.harness.get_relation_data(
-            relation_id, self.harness.charm.app.name
-        )
-
-        self.assertDictEqual(expected_result, relation_data)
-
-
-if __name__ == "__main__":
-    unittest.main()
diff --git a/installers/charm/prometheus/tests/test_pod_spec.py b/installers/charm/prometheus/tests/test_pod_spec.py
deleted file mode 100644 (file)
index 1adbae6..0000000
+++ /dev/null
@@ -1,640 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2020 Canonical Ltd.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-#         http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-from typing import NoReturn
-import unittest
-
-import pod_spec
-
-
-class TestPodSpec(unittest.TestCase):
-    """Pod spec unit tests."""
-
-    def test_make_pod_ports(self) -> NoReturn:
-        """Testing make pod ports."""
-        port = 9090
-
-        expected_result = [
-            {
-                "name": "prometheus",
-                "containerPort": port,
-                "protocol": "TCP",
-            }
-        ]
-
-        pod_ports = pod_spec._make_pod_ports(port)
-
-        self.assertListEqual(expected_result, pod_ports)
-
-    def test_make_pod_envconfig(self) -> NoReturn:
-        """Testing make pod envconfig."""
-        config = {}
-        relation_state = {}
-
-        expected_result = {}
-
-        pod_envconfig = pod_spec._make_pod_envconfig(config, relation_state)
-
-        self.assertDictEqual(expected_result, pod_envconfig)
-
-    def test_make_pod_ingress_resources_without_site_url(self) -> NoReturn:
-        """Testing make pod ingress resources without site_url."""
-        config = {"site_url": ""}
-        app_name = "prometheus"
-        port = 9090
-
-        pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-            config, app_name, port
-        )
-
-        self.assertIsNone(pod_ingress_resources)
-
-    def test_make_pod_ingress_resources(self) -> NoReturn:
-        """Testing make pod ingress resources."""
-        config = {
-            "site_url": "http://prometheus",
-            "max_file_size": 0,
-            "ingress_whitelist_source_range": "",
-        }
-        app_name = "prometheus"
-        port = 9090
-
-        expected_result = [
-            {
-                "name": f"{app_name}-ingress",
-                "annotations": {
-                    "nginx.ingress.kubernetes.io/proxy-body-size": f"{config['max_file_size']}",
-                    "nginx.ingress.kubernetes.io/ssl-redirect": "false",
-                },
-                "spec": {
-                    "rules": [
-                        {
-                            "host": app_name,
-                            "http": {
-                                "paths": [
-                                    {
-                                        "path": "/",
-                                        "backend": {
-                                            "serviceName": app_name,
-                                            "servicePort": port,
-                                        },
-                                    }
-                                ]
-                            },
-                        }
-                    ]
-                },
-            }
-        ]
-
-        pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-            config, app_name, port
-        )
-
-        self.assertListEqual(expected_result, pod_ingress_resources)
-
-    def test_make_pod_ingress_resources_with_whitelist_source_range(self) -> NoReturn:
-        """Testing make pod ingress resources with whitelist_source_range."""
-        config = {
-            "site_url": "http://prometheus",
-            "max_file_size": 0,
-            "ingress_whitelist_source_range": "0.0.0.0/0",
-        }
-        app_name = "prometheus"
-        port = 9090
-
-        expected_result = [
-            {
-                "name": f"{app_name}-ingress",
-                "annotations": {
-                    "nginx.ingress.kubernetes.io/proxy-body-size": f"{config['max_file_size']}",
-                    "nginx.ingress.kubernetes.io/ssl-redirect": "false",
-                    "nginx.ingress.kubernetes.io/whitelist-source-range": config[
-                        "ingress_whitelist_source_range"
-                    ],
-                },
-                "spec": {
-                    "rules": [
-                        {
-                            "host": app_name,
-                            "http": {
-                                "paths": [
-                                    {
-                                        "path": "/",
-                                        "backend": {
-                                            "serviceName": app_name,
-                                            "servicePort": port,
-                                        },
-                                    }
-                                ]
-                            },
-                        }
-                    ]
-                },
-            }
-        ]
-
-        pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-            config, app_name, port
-        )
-
-        self.assertListEqual(expected_result, pod_ingress_resources)
-
-    def test_make_pod_ingress_resources_with_https(self) -> NoReturn:
-        """Testing make pod ingress resources with HTTPs."""
-        config = {
-            "site_url": "https://prometheus",
-            "max_file_size": 0,
-            "ingress_whitelist_source_range": "",
-            "tls_secret_name": "",
-        }
-        app_name = "prometheus"
-        port = 9090
-
-        expected_result = [
-            {
-                "name": f"{app_name}-ingress",
-                "annotations": {
-                    "nginx.ingress.kubernetes.io/proxy-body-size": f"{config['max_file_size']}",
-                },
-                "spec": {
-                    "rules": [
-                        {
-                            "host": app_name,
-                            "http": {
-                                "paths": [
-                                    {
-                                        "path": "/",
-                                        "backend": {
-                                            "serviceName": app_name,
-                                            "servicePort": port,
-                                        },
-                                    }
-                                ]
-                            },
-                        }
-                    ],
-                    "tls": [{"hosts": [app_name]}],
-                },
-            }
-        ]
-
-        pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-            config, app_name, port
-        )
-
-        self.assertListEqual(expected_result, pod_ingress_resources)
-
-    def test_make_pod_ingress_resources_with_https_tls_secret_name(self) -> NoReturn:
-        """Testing make pod ingress resources with HTTPs and TLS secret name."""
-        config = {
-            "site_url": "https://prometheus",
-            "max_file_size": 0,
-            "ingress_whitelist_source_range": "",
-            "tls_secret_name": "secret_name",
-        }
-        app_name = "prometheus"
-        port = 9090
-
-        expected_result = [
-            {
-                "name": f"{app_name}-ingress",
-                "annotations": {
-                    "nginx.ingress.kubernetes.io/proxy-body-size": f"{config['max_file_size']}",
-                },
-                "spec": {
-                    "rules": [
-                        {
-                            "host": app_name,
-                            "http": {
-                                "paths": [
-                                    {
-                                        "path": "/",
-                                        "backend": {
-                                            "serviceName": app_name,
-                                            "servicePort": port,
-                                        },
-                                    }
-                                ]
-                            },
-                        }
-                    ],
-                    "tls": [
-                        {"hosts": [app_name], "secretName": config["tls_secret_name"]}
-                    ],
-                },
-            }
-        ]
-
-        pod_ingress_resources = pod_spec._make_pod_ingress_resources(
-            config, app_name, port
-        )
-
-        self.assertListEqual(expected_result, pod_ingress_resources)
-
-    def test_make_pod_files(self) -> NoReturn:
-        """Testing make pod files."""
-        config = {
-            "web_subpath": "/",
-            "default_target": "",
-            "site_url": "",
-        }
-
-        expected_result = [
-            {
-                "name": "config",
-                "mountPath": "/etc/prometheus",
-                "files": [
-                    {
-                        "path": "prometheus.yml",
-                        "content": (
-                            "global:\n"
-                            "  scrape_interval: 15s\n"
-                            "  evaluation_interval: 15s\n"
-                            "alerting:\n"
-                            "  alertmanagers:\n"
-                            "    - static_configs:\n"
-                            "        - targets:\n"
-                            "rule_files:\n"
-                            "scrape_configs:\n"
-                            "  - job_name: 'prometheus'\n"
-                            "    static_configs:\n"
-                            "      - targets: [{}]\n".format(config["default_target"])
-                        ),
-                    }
-                ],
-            }
-        ]
-
-        pod_envconfig = pod_spec._make_pod_files(config)
-        self.assertListEqual(expected_result, pod_envconfig)
-
-    def test_make_readiness_probe(self) -> NoReturn:
-        """Testing make readiness probe."""
-        port = 9090
-
-        expected_result = {
-            "httpGet": {
-                "path": "/-/ready",
-                "port": port,
-            },
-            "initialDelaySeconds": 10,
-            "timeoutSeconds": 30,
-        }
-
-        readiness_probe = pod_spec._make_readiness_probe(port)
-
-        self.assertDictEqual(expected_result, readiness_probe)
-
-    def test_make_liveness_probe(self) -> NoReturn:
-        """Testing make liveness probe."""
-        port = 9090
-
-        expected_result = {
-            "httpGet": {
-                "path": "/-/healthy",
-                "port": port,
-            },
-            "initialDelaySeconds": 30,
-            "periodSeconds": 30,
-        }
-
-        liveness_probe = pod_spec._make_liveness_probe(port)
-
-        self.assertDictEqual(expected_result, liveness_probe)
-
-    def test_make_pod_command(self) -> NoReturn:
-        """Testing make pod command."""
-        port = 9090
-        config = {
-            "web_subpath": "/",
-            "default_target": "",
-            "site_url": "",
-        }
-
-        expected_result = [
-            "/bin/prometheus",
-            "--config.file=/etc/prometheus/prometheus.yml",
-            "--storage.tsdb.path=/prometheus",
-            "--web.console.libraries=/usr/share/prometheus/console_libraries",
-            "--web.console.templates=/usr/share/prometheus/consoles",
-            "--web.route-prefix={}".format(config.get("web_subpath")),
-            "--web.external-url=http://localhost:{}{}".format(
-                port, config.get("web_subpath")
-            ),
-        ]
-
-        pod_envconfig = pod_spec._make_pod_command(config, port)
-
-        self.assertListEqual(expected_result, pod_envconfig)
-
-    def test_make_pod_command_with_web_admin_api_enabled(self) -> NoReturn:
-        """Testing make pod command."""
-        port = 9090
-        config = {
-            "web_subpath": "/",
-            "default_target": "",
-            "site_url": "",
-            "enable_web_admin_api": True,
-        }
-
-        expected_result = [
-            "/bin/prometheus",
-            "--config.file=/etc/prometheus/prometheus.yml",
-            "--storage.tsdb.path=/prometheus",
-            "--web.console.libraries=/usr/share/prometheus/console_libraries",
-            "--web.console.templates=/usr/share/prometheus/consoles",
-            "--web.route-prefix={}".format(config.get("web_subpath")),
-            "--web.external-url=http://localhost:{}{}".format(
-                port, config.get("web_subpath")
-            ),
-            "--web.enable-admin-api",
-        ]
-
-        pod_envconfig = pod_spec._make_pod_command(config, port)
-
-        self.assertListEqual(expected_result, pod_envconfig)
-
-    def test_make_pod_spec(self) -> NoReturn:
-        """Testing make pod spec."""
-        image_info = {"upstream-source": "ubuntu/prometheus:latest"}
-        config = {
-            "web_subpath": "/",
-            "default_target": "",
-            "site_url": "",
-            "enable_web_admin_api": False,
-        }
-        relation_state = {}
-        app_name = "prometheus"
-        port = 9090
-
-        expected_result = {
-            "version": 3,
-            "containers": [
-                {
-                    "name": app_name,
-                    "imageDetails": image_info,
-                    "imagePullPolicy": "Always",
-                    "ports": [
-                        {
-                            "name": app_name,
-                            "containerPort": port,
-                            "protocol": "TCP",
-                        }
-                    ],
-                    "envConfig": {},
-                    "volumeConfig": [
-                        {
-                            "name": "config",
-                            "mountPath": "/etc/prometheus",
-                            "files": [
-                                {
-                                    "path": "prometheus.yml",
-                                    "content": (
-                                        "global:\n"
-                                        "  scrape_interval: 15s\n"
-                                        "  evaluation_interval: 15s\n"
-                                        "alerting:\n"
-                                        "  alertmanagers:\n"
-                                        "    - static_configs:\n"
-                                        "        - targets:\n"
-                                        "rule_files:\n"
-                                        "scrape_configs:\n"
-                                        "  - job_name: 'prometheus'\n"
-                                        "    static_configs:\n"
-                                        "      - targets: [{}]\n".format(
-                                            config.get("default_target")
-                                        )
-                                    ),
-                                }
-                            ],
-                        }
-                    ],
-                    "command": [
-                        "/bin/prometheus",
-                        "--config.file=/etc/prometheus/prometheus.yml",
-                        "--storage.tsdb.path=/prometheus",
-                        "--web.console.libraries=/usr/share/prometheus/console_libraries",
-                        "--web.console.templates=/usr/share/prometheus/consoles",
-                        "--web.route-prefix={}".format(config.get("web_subpath")),
-                        "--web.external-url=http://localhost:{}{}".format(
-                            port, config.get("web_subpath")
-                        ),
-                    ],
-                    "kubernetes": {
-                        "readinessProbe": {
-                            "httpGet": {
-                                "path": "/-/ready",
-                                "port": port,
-                            },
-                            "initialDelaySeconds": 10,
-                            "timeoutSeconds": 30,
-                        },
-                        "livenessProbe": {
-                            "httpGet": {
-                                "path": "/-/healthy",
-                                "port": port,
-                            },
-                            "initialDelaySeconds": 30,
-                            "periodSeconds": 30,
-                        },
-                    },
-                }
-            ],
-            "kubernetesResources": {"ingressResources": []},
-        }
-
-        spec = pod_spec.make_pod_spec(
-            image_info, config, relation_state, app_name, port
-        )
-
-        self.assertDictEqual(expected_result, spec)
-
-    def test_make_pod_spec_with_ingress(self) -> NoReturn:
-        """Testing make pod spec."""
-        image_info = {"upstream-source": "ubuntu/prometheus:latest"}
-        config = {
-            "web_subpath": "/",
-            "default_target": "",
-            "site_url": "https://prometheus",
-            "tls_secret_name": "prometheus",
-            "max_file_size": 0,
-            "ingress_whitelist_source_range": "0.0.0.0/0",
-            "enable_web_admin_api": False,
-        }
-        relation_state = {}
-        app_name = "prometheus"
-        port = 9090
-
-        expected_result = {
-            "version": 3,
-            "containers": [
-                {
-                    "name": app_name,
-                    "imageDetails": image_info,
-                    "imagePullPolicy": "Always",
-                    "ports": [
-                        {
-                            "name": app_name,
-                            "containerPort": port,
-                            "protocol": "TCP",
-                        }
-                    ],
-                    "envConfig": {},
-                    "volumeConfig": [
-                        {
-                            "name": "config",
-                            "mountPath": "/etc/prometheus",
-                            "files": [
-                                {
-                                    "path": "prometheus.yml",
-                                    "content": (
-                                        "global:\n"
-                                        "  scrape_interval: 15s\n"
-                                        "  evaluation_interval: 15s\n"
-                                        "alerting:\n"
-                                        "  alertmanagers:\n"
-                                        "    - static_configs:\n"
-                                        "        - targets:\n"
-                                        "rule_files:\n"
-                                        "scrape_configs:\n"
-                                        "  - job_name: 'prometheus'\n"
-                                        "    static_configs:\n"
-                                        "      - targets: [{}]\n".format(
-                                            config.get("default_target")
-                                        )
-                                    ),
-                                }
-                            ],
-                        }
-                    ],
-                    "command": [
-                        "/bin/prometheus",
-                        "--config.file=/etc/prometheus/prometheus.yml",
-                        "--storage.tsdb.path=/prometheus",
-                        "--web.console.libraries=/usr/share/prometheus/console_libraries",
-                        "--web.console.templates=/usr/share/prometheus/consoles",
-                        "--web.route-prefix={}".format(config.get("web_subpath")),
-                        "--web.external-url=http://localhost:{}{}".format(
-                            port, config.get("web_subpath")
-                        ),
-                    ],
-                    "kubernetes": {
-                        "readinessProbe": {
-                            "httpGet": {
-                                "path": "/-/ready",
-                                "port": port,
-                            },
-                            "initialDelaySeconds": 10,
-                            "timeoutSeconds": 30,
-                        },
-                        "livenessProbe": {
-                            "httpGet": {
-                                "path": "/-/healthy",
-                                "port": port,
-                            },
-                            "initialDelaySeconds": 30,
-                            "periodSeconds": 30,
-                        },
-                    },
-                }
-            ],
-            "kubernetesResources": {
-                "ingressResources": [
-                    {
-                        "name": "{}-ingress".format(app_name),
-                        "annotations": {
-                            "nginx.ingress.kubernetes.io/proxy-body-size": str(
-                                config.get("max_file_size")
-                            ),
-                            "nginx.ingress.kubernetes.io/whitelist-source-range": config.get(
-                                "ingress_whitelist_source_range"
-                            ),
-                        },
-                        "spec": {
-                            "rules": [
-                                {
-                                    "host": app_name,
-                                    "http": {
-                                        "paths": [
-                                            {
-                                                "path": "/",
-                                                "backend": {
-                                                    "serviceName": app_name,
-                                                    "servicePort": port,
-                                                },
-                                            }
-                                        ]
-                                    },
-                                }
-                            ],
-                            "tls": [
-                                {
-                                    "hosts": [app_name],
-                                    "secretName": config.get("tls_secret_name"),
-                                }
-                            ],
-                        },
-                    }
-                ],
-            },
-        }
-
-        spec = pod_spec.make_pod_spec(
-            image_info, config, relation_state, app_name, port
-        )
-
-        self.assertDictEqual(expected_result, spec)
-
-    def test_make_pod_spec_without_image_info(self) -> NoReturn:
-        """Testing make pod spec without image_info."""
-        image_info = None
-        config = {
-            "web_subpath": "/",
-            "default_target": "",
-            "site_url": "",
-            "enable_web_admin_api": False,
-        }
-        relation_state = {}
-        app_name = "prometheus"
-        port = 9090
-
-        spec = pod_spec.make_pod_spec(
-            image_info, config, relation_state, app_name, port
-        )
-
-        self.assertIsNone(spec)
-
-    def test_make_pod_spec_without_config(self) -> NoReturn:
-        """Testing make pod spec without config."""
-        image_info = {"upstream-source": "ubuntu/prometheus:latest"}
-        config = {}
-        relation_state = {}
-        app_name = "prometheus"
-        port = 9090
-
-        with self.assertRaises(ValueError):
-            pod_spec.make_pod_spec(image_info, config, relation_state, app_name, port)
-
-
-if __name__ == "__main__":
-    unittest.main()
diff --git a/installers/charm/prometheus/tox.ini b/installers/charm/prometheus/tox.ini
deleted file mode 100644 (file)
index 4c7970d..0000000
+++ /dev/null
@@ -1,126 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-#######################################################################################
-
-[tox]
-envlist = black, cover, flake8, pylint, yamllint, safety
-skipsdist = true
-
-[tox:jenkins]
-toxworkdir = /tmp/.tox
-
-[testenv]
-basepython = python3.8
-setenv = VIRTUAL_ENV={envdir}
-         PYTHONDONTWRITEBYTECODE = 1
-deps =  -r{toxinidir}/requirements.txt
-
-
-#######################################################################################
-[testenv:black]
-deps = black
-commands =
-        black --check --diff src/ tests/
-
-
-#######################################################################################
-[testenv:cover]
-deps =  {[testenv]deps}
-        -r{toxinidir}/requirements-test.txt
-        coverage
-        nose2
-commands =
-        sh -c 'rm -f nosetests.xml'
-        coverage erase
-        nose2 -C --coverage src
-        coverage report --omit='*tests*'
-        coverage html -d ./cover --omit='*tests*'
-        coverage xml -o coverage.xml --omit=*tests*
-whitelist_externals = sh
-
-
-#######################################################################################
-[testenv:flake8]
-deps =  flake8
-        flake8-import-order
-commands =
-        flake8 src/ tests/
-
-
-#######################################################################################
-[testenv:pylint]
-deps =  {[testenv]deps}
-        -r{toxinidir}/requirements-test.txt
-        pylint==2.10.2
-commands =
-    pylint -E src/ tests/
-
-
-#######################################################################################
-[testenv:safety]
-setenv =
-        LC_ALL=C.UTF-8
-        LANG=C.UTF-8
-deps =  {[testenv]deps}
-        safety
-commands =
-        - safety check --full-report
-
-
-#######################################################################################
-[testenv:yamllint]
-deps =  {[testenv]deps}
-        -r{toxinidir}/requirements-test.txt
-        yamllint
-commands = yamllint .
-
-#######################################################################################
-[testenv:build]
-passenv=HTTP_PROXY HTTPS_PROXY NO_PROXY
-whitelist_externals =
-  charmcraft
-  sh
-commands =
-  charmcraft pack
-  sh -c 'ubuntu_version=20.04; \
-        architectures="amd64-aarch64-arm64"; \
-        charm_name=`cat metadata.yaml | grep -E "^name: " | cut -f 2 -d " "`; \
-        mv $charm_name"_ubuntu-"$ubuntu_version-$architectures.charm $charm_name.charm'
-
-#######################################################################################
-[flake8]
-ignore =
-        W291,
-        W293,
-        W503,
-        E123,
-        E125,
-        E226,
-        E241,
-exclude =
-        .git,
-        __pycache__,
-        .tox,
-max-line-length = 120
-show-source = True
-builtins = _
-max-complexity = 10
-import-order-style = google
diff --git a/installers/charm/vca-integrator-operator/.gitignore b/installers/charm/vca-integrator-operator/.gitignore
deleted file mode 100644 (file)
index 9ac35bd..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-#######################################################################################
-# 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
deleted file mode 100644 (file)
index 5cee024..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-#######################################################################################
-# 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
deleted file mode 100644 (file)
index 32a5d04..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-<!--
-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.
--->
-
-
-# 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="<root>=INFO;unit=DEBUG"
-# Deploy the charm
-juju deploy ./osm-vca-integrator_ubuntu-22.04-amd64.charm --series jammy
-```
-
diff --git a/installers/charm/vca-integrator-operator/LICENSE b/installers/charm/vca-integrator-operator/LICENSE
deleted file mode 100644 (file)
index d645695..0000000
+++ /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/installers/charm/vca-integrator-operator/README.md b/installers/charm/vca-integrator-operator/README.md
deleted file mode 100644 (file)
index 140af91..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-<!--
-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.
--->
-
-# 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
deleted file mode 100644 (file)
index 65d82b9..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-#######################################################################################
-# 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
deleted file mode 100644 (file)
index 199e221..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-#######################################################################################
-# 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: "22.04"
-    run-on:
-      - name: "ubuntu"
-        channel: "22.04"
-parts:
-  charm:
-    charm-binary-python-packages: [cryptography, bcrypt]
-    build-packages:
-      - libffi-dev
diff --git a/installers/charm/vca-integrator-operator/config.yaml b/installers/charm/vca-integrator-operator/config.yaml
deleted file mode 100644 (file)
index 97b36cb..0000000
+++ /dev/null
@@ -1,116 +0,0 @@
-#######################################################################################
-# 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:
-        <lxd-cloud-name>[:<lxd-credential-name>]
-
-        By default, the <lxd-credential-name> will be the same as
-        <lxd-cloud-name>.
-    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:
-        <k8s-cloud-name>[:<k8s-credential-name>]
-
-        By default, the <k8s-credential-name> will be the same as
-        <k8s-cloud-name>.
-    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
deleted file mode 100644 (file)
index 21dac69..0000000
+++ /dev/null
@@ -1,221 +0,0 @@
-# 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
deleted file mode 100644 (file)
index bcc4375..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-#######################################################################################
-# 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 <david.garcia@canonical.com>
-
-provides:
-  vca:
-    interface: osm-vca
diff --git a/installers/charm/vca-integrator-operator/pyproject.toml b/installers/charm/vca-integrator-operator/pyproject.toml
deleted file mode 100644 (file)
index 7f5495b..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-#######################################################################################
-# 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"
-
-[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
deleted file mode 100644 (file)
index 65d82b9..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-#######################################################################################
-# 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
deleted file mode 100644 (file)
index 387a2e0..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-#######################################################################################
-# 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 < 2.2
-juju < 3
-pyyaml
diff --git a/installers/charm/vca-integrator-operator/src/charm.py b/installers/charm/vca-integrator-operator/src/charm.py
deleted file mode 100755 (executable)
index 34cb4f9..0000000
+++ /dev/null
@@ -1,213 +0,0 @@
-#!/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
deleted file mode 100644 (file)
index 8d69e7b..0000000
+++ /dev/null
@@ -1,193 +0,0 @@
-#!/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 asyncio
-import logging
-import shlex
-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())
-VCA_APP = "osm-vca"
-
-LCM_CHARM = "osm-lcm"
-LCM_APP = "lcm"
-KAFKA_CHARM = "kafka-k8s"
-KAFKA_APP = "kafka"
-MONGO_DB_CHARM = "mongodb-k8s"
-MONGO_DB_APP = "mongodb"
-RO_CHARM = "osm-ro"
-RO_APP = "ro"
-ZOOKEEPER_CHARM = "zookeeper-k8s"
-ZOOKEEPER_APP = "zookeeper"
-LCM_APPS = [KAFKA_APP, MONGO_DB_APP, ZOOKEEPER_APP, RO_APP, LCM_APP]
-MON_CHARM = "osm-mon"
-MON_APP = "mon"
-KEYSTONE_CHARM = "osm-keystone"
-KEYSTONE_APP = "keystone"
-MARIADB_CHARM = "charmed-osm-mariadb-k8s"
-MARIADB_APP = "mariadb"
-PROMETHEUS_CHARM = "osm-prometheus"
-PROMETHEUS_APP = "prometheus"
-MON_APPS = [
-    KAFKA_APP,
-    ZOOKEEPER_APP,
-    KEYSTONE_APP,
-    MONGO_DB_APP,
-    MARIADB_APP,
-    PROMETHEUS_APP,
-    MON_APP,
-]
-
-
-@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.
-    """
-    charm = await ops_test.build_charm(".")
-    await ops_test.model.deploy(charm, application_name=VCA_APP, series="jammy")
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=[VCA_APP],
-            status="blocked",
-        )
-    assert ops_test.model.applications[VCA_APP].units[0].workload_status == "blocked"
-
-
-@pytest.mark.abort_on_fail
-async def test_vca_configuration(ops_test: OpsTest):
-    controllers = (Path.home() / ".local/share/juju/controllers.yaml").read_text()
-    accounts = (Path.home() / ".local/share/juju/accounts.yaml").read_text()
-    public_key = (Path.home() / ".local/share/juju/ssh/juju_id_rsa.pub").read_text()
-    await ops_test.model.applications[VCA_APP].set_config(
-        {
-            "controllers": controllers,
-            "accounts": accounts,
-            "public-key": public_key,
-            "k8s-cloud": "microk8s",
-        }
-    )
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=[VCA_APP],
-            status="active",
-        )
-
-
-@pytest.mark.abort_on_fail
-async def test_vca_integration_lcm(ops_test: OpsTest):
-    lcm_deploy_cmd = f"juju deploy {LCM_CHARM} {LCM_APP} --resource lcm-image=opensourcemano/lcm:testing-daily --channel=latest/beta --series=jammy"
-    ro_deploy_cmd = f"juju deploy {RO_CHARM} {RO_APP} --resource ro-image=opensourcemano/ro:testing-daily --channel=latest/beta --series=jammy"
-
-    await asyncio.gather(
-        # LCM and RO charms have to be deployed differently since
-        # bug https://github.com/juju/python-libjuju/pull/820
-        # fails to parse assumes
-        ops_test.run(*shlex.split(lcm_deploy_cmd), check=True),
-        ops_test.run(*shlex.split(ro_deploy_cmd), check=True),
-        ops_test.model.deploy(KAFKA_CHARM, application_name=KAFKA_APP, channel="stable"),
-        ops_test.model.deploy(MONGO_DB_CHARM, application_name=MONGO_DB_APP, channel="5/edge"),
-        ops_test.model.deploy(ZOOKEEPER_CHARM, application_name=ZOOKEEPER_APP, channel="stable"),
-    )
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=LCM_APPS,
-        )
-    # wait for MongoDB to be active before relating RO to it
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(apps=[MONGO_DB_APP], status="active")
-    logger.info("Adding relations")
-    await ops_test.model.add_relation(KAFKA_APP, ZOOKEEPER_APP)
-    await ops_test.model.add_relation(
-        "{}:mongodb".format(RO_APP), "{}:database".format(MONGO_DB_APP)
-    )
-    await ops_test.model.add_relation(RO_APP, KAFKA_APP)
-    # LCM specific
-    await ops_test.model.add_relation(
-        "{}:mongodb".format(LCM_APP), "{}:database".format(MONGO_DB_APP)
-    )
-    await ops_test.model.add_relation(LCM_APP, KAFKA_APP)
-    await ops_test.model.add_relation(LCM_APP, RO_APP)
-
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=LCM_APPS,
-            status="active",
-        )
-
-    logger.info("Adding relation VCA LCM")
-    await ops_test.model.add_relation(VCA_APP, LCM_APP)
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=[VCA_APP, LCM_APP],
-            status="active",
-        )
-
-
-@pytest.mark.abort_on_fail
-async def test_vca_integration_mon(ops_test: OpsTest):
-    keystone_image = "opensourcemano/keystone:testing-daily"
-    keystone_deploy_cmd = f"juju deploy {KEYSTONE_CHARM} {KEYSTONE_APP} --resource keystone-image={keystone_image} --channel=latest/beta --series jammy"
-    mon_deploy_cmd = f"juju deploy {MON_CHARM} {MON_APP} --resource mon-image=opensourcemano/mon:testing-daily --channel=latest/beta --series=jammy"
-    await asyncio.gather(
-        # MON charm has to be deployed differently since
-        # bug https://github.com/juju/python-libjuju/issues/820
-        # fails to parse assumes
-        ops_test.run(*shlex.split(mon_deploy_cmd), check=True),
-        ops_test.model.deploy(MARIADB_CHARM, application_name=MARIADB_APP, channel="stable"),
-        ops_test.model.deploy(PROMETHEUS_CHARM, application_name=PROMETHEUS_APP, channel="stable"),
-        # Keystone charm has to be deployed differently since
-        # bug https://github.com/juju/python-libjuju/issues/766
-        # prevents setting correctly the resources
-        ops_test.run(*shlex.split(keystone_deploy_cmd), check=True),
-    )
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=MON_APPS,
-        )
-
-    logger.info("Adding relations")
-    await ops_test.model.add_relation(MARIADB_APP, KEYSTONE_APP)
-    # MON specific
-    await ops_test.model.add_relation(
-        "{}:mongodb".format(MON_APP), "{}:database".format(MONGO_DB_APP)
-    )
-    await ops_test.model.add_relation(MON_APP, KAFKA_APP)
-    await ops_test.model.add_relation(MON_APP, KEYSTONE_APP)
-    await ops_test.model.add_relation(MON_APP, PROMETHEUS_APP)
-
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=MON_APPS,
-            status="active",
-        )
-
-    logger.info("Adding relation VCA MON")
-    await ops_test.model.add_relation(VCA_APP, MON_APP)
-    async with ops_test.fast_forward():
-        await ops_test.model.wait_for_idle(
-            apps=[VCA_APP, MON_APP],
-            status="active",
-        )
diff --git a/installers/charm/vca-integrator-operator/tests/unit/test_charm.py b/installers/charm/vca-integrator-operator/tests/unit/test_charm.py
deleted file mode 100644 (file)
index 5018675..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-#######################################################################################
-# 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
deleted file mode 100644 (file)
index a8eb8bc..0000000
+++ /dev/null
@@ -1,106 +0,0 @@
-#######################################################################################
-# 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]
-basepython = python3.8
-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-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<3
-    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} --cloud microk8s
diff --git a/installers/charm/zookeeper-k8s/.gitignore b/installers/charm/zookeeper-k8s/.gitignore
deleted file mode 100644 (file)
index 712eb96..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-release/
-__pycache__
-.tox
diff --git a/installers/charm/zookeeper-k8s/.yamllint.yaml b/installers/charm/zookeeper-k8s/.yamllint.yaml
deleted file mode 100644 (file)
index 21b95b5..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
----
-
-extends: default
-rules:
-  line-length: disable
-yaml-files:
-  - '*.yaml'
-  - '*.yml'
-  - '.yamllint'
-ignore: |
- reactive/
- .tox
- release/
diff --git a/installers/charm/zookeeper-k8s/README.md b/installers/charm/zookeeper-k8s/README.md
deleted file mode 100755 (executable)
index 442fbb2..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
-<!-- 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.
-
-For those usages not covered by the Apache License, Version 2.0 please
-contact: legal@canonical.com
-
-To get in touch with the maintainers, please contact:
-osm-charmers@lists.launchpad.net -->
-
-# Overview
-
-Zookeeper for Juju CAAS
-
-
-## Testing
-
-The tests of this charm are done using tox and Zaza.
-
-
-
-### Prepare environment
-
-The machine in which the tests are run needs access to a juju k8s controller. The easiest way to approach this is by executing the following commands:
-
-```
-sudo apt install tox -y
-sudo snap install microk8s --classic
-sudo snap install juju
-
-microk8s.status --wait-ready
-microk8s.enable storage dashboard dns
-
-juju bootstrap microk8s k8s-cloud
-```
-
-If /usr/bin/python does not exist, you should probably need to do this:
-```
-sudo ln -s /usr/bin/python3 /usr/bin/python
-```
-
-### Build Charm
-
-**Download dependencies:**
-```
-mkdir -p ~/charm/layers ~/charm/builds
-cd ~/charm/layers
-git clone https://git.launchpad.net/charm-k8s-zookeeper zookeeper-k8s
-git clone https://git.launchpad.net/charm-osm-common osm-common
-```
-
-**Charm structure:**
-```
-├── config.yaml
-├── icon.svg
-├── layer.yaml
-├── metadata.yaml
-├── reactive
-│   ├── spec_template.yaml
-│   └── zookeeper.py
-├── README.md
-├── test-requirements.txt
-├── tests
-│   ├── basic_deployment.py
-│   ├── bundles
-│   │   ├── zookeeper-ha.yaml
-│   │   └── zookeeper.yaml
-│   └── tests.yaml
-└── tox.ini
-```
-
-**Setup environment variables:**
-
-```
-export CHARM_LAYERS_DIR=~/charm/layers
-export CHARM_BUILD_DIR=~/charm/builds
-```
-
-**Build:**
-```
-charm build ~/charm/layers/zookeeper-k8s
-mkdir ~/charm/layers/zookeeper-k8s/tests/build/
-mv ~/charm/builds/zookeeper-k8s ~/charm/layers/zookeeper-k8s/tests/build/
-```
-
-### Test charm with Tox
-
-```
-cd ~/charm/layers/zookeeper-k8s
-tox -e func
-```
\ No newline at end of file
diff --git a/installers/charm/zookeeper-k8s/config.yaml b/installers/charm/zookeeper-k8s/config.yaml
deleted file mode 100755 (executable)
index fe04908..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-options:
-    client-port:
-        description: Zookeeper client port
-        type: int
-        default: 2181
-    server-port:
-        description: Zookeeper server port
-        type: int
-        default: 2888
-    leader-election-port:
-        description: Zookeeper leader-election port
-        type: int
-        default: 3888
-    zookeeper-units:
-        description: Zookeeper zookeeper-units
-        type: int
-        default: 1
-    image:
-        description: Zookeeper image to use
-        type: string
-        default: rocks.canonical.com:443/k8s.gcr.io/kubernetes-zookeeper:1.0-3.4.10
diff --git a/installers/charm/zookeeper-k8s/icon.svg b/installers/charm/zookeeper-k8s/icon.svg
deleted file mode 100644 (file)
index 0185a7e..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg width="100px" height="100px" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
-    <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
-    <title>zookeeper</title>
-    <desc>Created with Sketch.</desc>
-    <defs>
-        <circle id="path-1" cx="50" cy="50" r="50"></circle>
-        <circle id="path-3" cx="50" cy="50" r="50"></circle>
-    </defs>
-    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
-        <g id="zookeeper">
-            <g id="v1">
-                <mask id="mask-2" fill="white">
-                    <use xlink:href="#path-1"></use>
-                </mask>
-                <use id="Oval-Copy" fill="#EDDDD1" transform="translate(50.000000, 50.000000) rotate(-180.000000) translate(-50.000000, -50.000000) " xlink:href="#path-1"></use>
-                <g id="lockup" mask="url(#mask-2)" fill-rule="nonzero">
-                    <g transform="translate(15.000000, 31.000000)">
-                        <path d="M4.3095293,0.660486479 C3.54834732,0.660486479 2.52991239,0.783788028 1.65603042,1.54819493 C0.755883662,2.33557465 0.492957746,3.47214493 0.492957746,4.32108225 L0.492957746,6.57790493 C0.492957746,7.42103423 0.772815775,8.59377676 1.72535211,9.34886676 C2.62971563,10.0657673 3.57504493,10.0960054 4.17473592,10.0960054 L9.67814676,10.0960054 L1.84859155,18.711213 C1.84730081,18.7124895 1.84601713,18.7137732 1.84474056,18.7150639 L1.84281507,18.7169894 L1.82163268,18.7400963 L1.80045127,18.7612787 L1.79274831,18.7709062 C1.73557404,18.8341239 1.69576679,18.9110851 1.67721183,18.9942782 C0.852378169,20.0028145 0.492957746,21.3000151 0.492957746,22.1002975 L0.492957746,24.9001427 C0.492957746,25.8009904 0.896039437,26.8789569 1.76001296,27.5074277 C2.55611507,28.0865239 3.34990225,28.1236249 3.78961268,28.1236249 L6.33720254,28.1236249 C6.62385944,28.1230886 6.89880268,28.0364799 7.16714324,27.9387658 C7.43548282,28.0364799 7.71042704,28.1230886 7.99708394,28.1236249 L19.9359044,28.1236249 L19.9455328,28.1236249 C20.0033878,28.1233775 20.0607477,28.1129484 20.114987,28.0928151 C20.826262,28.0831531 21.6527835,27.9308824 22.3621813,27.3110146 C23.1796304,26.5967287 23.4501538,25.5820886 23.4501538,24.8173415 L23.4501538,23.7216661 L26.5638751,26.8353873 C27.2099208,27.4881896 28.411323,28.1236249 29.6621924,28.1236249 L37.8864983,28.1236249 C38.1131169,28.123547 38.3370104,28.0869982 38.5546877,28.0254179 C39.3501746,28.1767027 40.1775648,27.9813179 40.7575927,27.4015187 L43.647942,24.5111685 C43.6485859,24.5105284 43.6492281,24.5098866 43.6498685,24.509243 L45.1248897,23.0149648 L47.137159,24.8731848 L49.0993615,26.8353873 C49.7454063,27.4881886 50.9468114,28.1236249 52.1976779,28.1236249 L60.4219848,28.1236249 C60.5088173,28.1235707 60.5921932,28.0952177 60.6780921,28.0851121 L60.6780921,27.8983275 C60.6780921,27.4616941 61.0290356,27.1107506 61.465669,27.1107506 L63.5838473,27.1107506 L67.1346831,23.5618403 L67.1135017,23.5753187 C67.1325615,23.5612299 67.1505805,23.5457849 67.1674185,23.5291039 C67.9170228,22.7724168 68.5211268,21.4505723 68.5211268,20.2709618 L68.5211268,8.75770197 C68.521128,8.75706014 68.521128,8.75641831 68.5211268,8.75577648 L68.5134248,7.45598592 C68.5133366,7.44248579 68.5126939,7.42899498 68.5114983,7.41554761 C68.4615913,6.84774901 68.2141975,6.30414282 67.7990206,5.88853408 L63.8149203,1.90636028 L63.8110693,1.90058282 C63.782113,1.87099549 63.7528963,1.8420293 63.7224907,1.8139307 C63.7205743,1.8126335 63.7186487,1.81134981 63.7167142,1.81007972 C62.9803546,1.14807789 61.7618459,0.660486479 60.6626872,0.660486479 L52.5346608,0.660486479 C52.534019,0.660485226 52.5333772,0.660485226 52.5327354,0.660486479 L51.3022665,0.664337465 C51.2945652,0.664156939 51.2868608,0.664156939 51.2791596,0.664337465 C50.6811308,0.695886761 50.1025996,0.945940563 49.665493,1.38259268 C49.6648494,1.38323273 49.6642075,1.38387457 49.6635675,1.38451817 L45.2385017,5.83654282 L44.5279485,5.15294845 L41.2794348,1.90636028 L41.2755828,1.90058282 C41.2466265,1.87099549 41.2174108,1.8420293 41.1870052,1.8139307 C41.1850885,1.8126335 41.1831626,1.8113498 41.1812277,1.81007972 C40.4448682,1.14807789 39.2263594,0.660486479 38.1272007,0.660486479 L29.9991744,0.660486479 C29.9978907,0.660481465 29.996607,0.660481465 29.9953234,0.660486479 L28.6916808,0.668188451 C28.678181,0.668276689 28.6646906,0.66891939 28.6512435,0.67011493 C28.085187,0.72113507 27.5443246,0.96870338 27.1300065,1.38259268 L23.4501538,5.06437085 L23.4501538,4.31145479 C23.4501538,3.67069549 23.3527848,2.60103732 22.5066021,1.72150113 C21.6578551,0.839291127 20.5415059,0.660486479 19.691351,0.660486479 L4.3095293,0.660486479 Z M31.3374777,10.0960054 L36.5501214,10.0960054 L36.5501214,18.6881051 L31.3374777,18.6881051 L31.3374777,10.0960054 Z M53.8729642,10.0960054 L59.0856079,10.0960054 L59.0856079,18.6881051 L53.8729642,18.6881051 L53.8729642,10.0960054 Z M15.316351,11.2224907 L15.5878632,11.2224907 L5.42253521,22.9494941 L5.42253521,22.1060739 C5.42746479,22.1008486 5.43305,22.0959437 5.43794014,22.090669 C5.43858757,22.0893882 5.43922941,22.0881045 5.43986563,22.086818 L15.316351,11.2224907 Z M21.9019589,11.4631931 L21.9019589,19.3197072 C21.2429513,18.9074545 20.5276844,18.6881051 19.9204994,18.6881051 L15.6398545,18.6881051 L21.9019589,11.4631931 Z" id="path4241" fill="#C39A7A"></path>
-                        <rect id="rect4282" fill="#C39A7A" x="18.0489213" y="19.1813073" width="5.34176182" height="1.49971815"></rect>
-                        <path d="M4.3095293,3.12550887 C3.36706901,3.12550887 2.95774648,3.47018493 2.95774648,4.32131592 L2.95774648,6.57813859 C2.95774648,7.40967254 3.34926732,7.63145028 4.17473592,7.63145028 L20.9853651,7.63145028 L20.9853651,4.31168845 C20.9853651,3.53962099 20.7397863,3.12550887 19.691351,3.12550887 L4.3095293,3.12550887 L4.3095293,3.12550887 Z M28.872689,3.12550887 L24.9675399,7.03065803 C24.5084286,7.45290394 24.3667476,7.87889042 24.3667476,8.66358056 L24.3667476,20.0266421 L28.872689,20.0266421 L28.872689,3.12550887 L28.872689,3.12550887 Z M29.9991744,3.12550887 L29.9991744,7.63145028 L43.5208515,7.63145028 L39.5329003,3.64349901 C39.101788,3.25592085 38.8033238,3.12550887 38.1272007,3.12550887 L29.9991744,3.12550887 L29.9991744,3.12550887 Z M51.4081755,3.12550887 L47.5030263,7.03065803 C47.0439151,7.45290394 46.9022341,7.87889042 46.9022341,8.66358056 L46.9022341,20.0266421 L51.4081755,20.0266421 L51.4081755,3.12550887 L51.4081755,3.12550887 Z M52.5346608,3.12550887 L52.5346608,7.63145028 L66.056338,7.63145028 L62.0683868,3.64349901 C61.6372745,3.25592085 61.3388103,3.12550887 60.6626872,3.12550887 L52.5346608,3.12550887 L52.5346608,3.12550887 Z M14.2245265,8.75793563 L3.61630746,20.4290958 C2.96621254,21.1009834 2.95774648,21.4137296 2.95774648,22.1005301 L2.95774648,24.9003763 C2.95774648,25.5239837 3.35458042,25.6590689 3.78961268,25.6590689 L6.33720254,25.6590689 L20.9853651,8.75793563 L14.2245265,8.75793563 Z M39.0149101,8.75793563 L39.0149101,25.6590689 L42.879622,21.794357 C43.3915694,21.2773715 43.5208515,21.0276332 43.5208515,20.2711955 L43.5208515,8.75793563 L39.0149101,8.75793563 Z M61.5503966,8.75793563 L61.5503966,25.6590689 L65.4151075,21.794357 C65.9270549,21.2773715 66.056338,21.0276332 66.056338,20.2711955 L66.056338,8.75793563 L61.5503966,8.75793563 Z M11.9715558,21.1531275 L7.99708394,25.6590689 L19.9455328,25.6590689 C20.6804746,25.693221 20.9853651,25.3596996 20.9853651,24.8175752 L20.9853651,22.1679273 C20.9853651,21.5235635 20.6978869,21.1531275 19.9204994,21.1531275 L11.9715558,21.1531275 Z M24.3667476,21.1531275 L28.3142606,25.1006404 C28.6683906,25.4590069 28.9949514,25.6590689 29.6621924,25.6590689 L37.8864983,25.6590689 L37.8864983,21.1531275 L24.3667476,21.1531275 Z M46.9022341,21.1531275 L50.849747,25.1006404 C51.203877,25.4590069 51.5304369,25.6590689 52.1976779,25.6590689 L60.4219848,25.6590689 L60.4219848,21.1531275 L46.9022341,21.1531275 Z" id="path4268" fill="#FFFFFF"></path>
-                        <path d="M0.492957746,25.6280263 L0.492957746,38.4391503 L3.29472944,38.4391503 L3.29472944,34.4974138 L4.50016493,33.149483 L7.56767141,38.4391503 L10.8720293,38.4391503 L6.3237231,31.1256597 L10.6910211,25.6280263 L7.43480451,25.6280263 L3.29472944,31.3162963 L3.29472944,25.6280263 L0.492957746,25.6280263 Z M12.1641179,25.6280263 L12.1641179,38.4391503 L21.7459841,38.4391503 L21.7459841,36.2728327 L14.9658896,36.2728327 L14.9658896,33.035871 L20.777399,33.035871 L20.777399,30.8695534 L14.9658896,30.8695534 L14.9658896,27.7924185 L21.3550834,27.7924185 L21.3550834,25.6280263 L12.1641179,25.6280263 Z M23.8256496,25.6280263 L23.8256496,38.4391503 L33.4055893,38.4391503 L33.4055893,36.2728327 L26.6254948,36.2728327 L26.6254948,33.035871 L32.4370052,33.035871 L32.4370052,30.8695534 L26.6254948,30.8695534 L26.6254948,27.7924185 L33.0166151,27.7924185 L33.0166151,25.6280263 L23.8256496,25.6280263 Z M35.4756273,25.6280263 L35.4756273,38.4391503 L38.277399,38.4391503 L38.277399,33.9178039 L40.1490975,33.9178039 C40.9910851,33.9178039 41.7186661,33.8081731 42.3327465,33.5865975 C42.9468279,33.3586913 43.4532217,33.0504024 43.852058,32.6642272 C44.2508944,32.2780539 44.5459937,31.8290858 44.7359155,31.3162963 C44.9321689,30.7971763 45.0305346,30.2498424 45.0305346,29.6737454 C45.0305346,29.0470028 44.928871,28.4831824 44.726287,27.9830541 C44.5300346,27.4829268 44.2250427,27.0584123 43.8135452,26.7102224 C43.4020477,26.3620315 42.8843899,26.0966073 42.2576473,25.913017 C41.6309048,25.7230952 40.8901352,25.6280263 40.0354865,25.6280263 L35.4756273,25.6280263 Z M47.1467865,25.6280263 L47.1467865,38.4391503 L56.7286527,38.4391503 L56.7286527,36.2728327 L49.9485582,36.2728327 L49.9485582,33.035871 L55.7600686,33.035871 L55.7600686,30.8695534 L49.9485582,30.8695534 L49.9485582,27.7924185 L56.3396785,27.7924185 L56.3396785,25.6280263 L47.1467865,25.6280263 Z M58.8083192,25.6280263 L58.8083192,38.4391503 L61.6100899,38.4391503 L61.6100899,33.5480856 L63.2526408,33.5480856 L66.0062723,38.4391503 L69.1585054,38.4391503 L65.84452,32.9203345 C66.2306952,32.8063814 66.586519,32.6492166 66.9093856,32.4466327 C67.2322532,32.2377192 67.5108661,31.9901538 67.7451038,31.7052706 C67.9793406,31.4140568 68.1628845,31.0780173 68.2958293,30.6981737 C68.4287751,30.318329 68.4941679,29.895741 68.4941679,29.4272665 C68.4941679,28.1991056 68.0795461,27.2583046 67.2502206,26.6062389 C66.4208931,25.9541731 65.1704328,25.6280263 63.4991197,25.6280263 L58.8083192,25.6280263 Z M38.277399,27.8020469 L39.7216104,27.8020469 C40.5509369,27.8020469 41.1702585,27.9512445 41.5817559,28.2487899 C41.9995849,28.5400037 42.209507,29.0318355 42.209507,29.7218856 C42.209507,30.3929431 42.0160714,30.9026358 41.6298962,31.2508256 C41.243722,31.5926849 40.6323666,31.7630393 39.7967096,31.7630393 L38.277399,31.7630393 L38.277399,27.8020469 Z M61.6100899,28.0618603 L63.2045006,28.0618603 C63.5970054,28.0618603 63.9462344,28.0948392 64.2501103,28.1581448 C64.5603148,28.2151208 64.8191442,28.3134886 65.0280587,28.4527639 C65.2369732,28.5857097 65.3960635,28.7626579 65.5036861,28.9842345 C65.6176392,29.2058092 65.6750658,29.347921 65.6750658,29.6707886 C65.6750658,30.2658773 65.484928,30.5915331 65.1050834,30.9080692 C64.7252407,31.2182756 64.1171823,31.374069 63.2815252,31.374069 L61.6100899,31.374069 L61.6100899,28.0618603 Z" id="path4243" fill="#C39A7A"></path>
-                        <polygon id="rect4314" fill="#C39A7A" points="0.985915493 25.6598931 22.0067085 25.6598931 24.5439472 25.6598931 27.461773 25.6598931 60.4225379 25.6598931 60.4225379 28.1236249 0.985915493 28.1236249"></polygon>
-                        <rect id="rect4280" fill="#C39A7A" x="0.492957746" y="23.5469392" width="1.42224294" height="6.90277552"></rect>
-                        <rect id="rect4284" fill="#C39A7A" x="22.0386777" y="23.6746763" width="1" height="3.26488183"></rect>
-                    </g>
-                </g>
-            </g>
-            <g id="v2">
-                <mask id="mask-4" fill="white">
-                    <use xlink:href="#path-3"></use>
-                </mask>
-                <use id="Oval-Copy" fill="#EDDDD1" transform="translate(50.000000, 50.000000) rotate(-180.000000) translate(-50.000000, -50.000000) " xlink:href="#path-3"></use>
-                <image id="zk_logo_use2" mask="url(#mask-4)" x="22" y="10" width="52.756654" height="75" xlink:href="data:image/png;base64,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"></image>
-            </g>
-        </g>
-    </g>
-</svg>
\ No newline at end of file
diff --git a/installers/charm/zookeeper-k8s/layer.yaml b/installers/charm/zookeeper-k8s/layer.yaml
deleted file mode 100644 (file)
index 88e0fc0..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-includes:
-  - "layer:caas-base"
-  - 'layer:osm-common'
-  - 'layer:status'
-  - 'layer:leadership'
-  - "interface:zookeeper"
-
-repo: https://code.launchpad.net/osm-k8s-bundle
diff --git a/installers/charm/zookeeper-k8s/metadata.yaml b/installers/charm/zookeeper-k8s/metadata.yaml
deleted file mode 100755 (executable)
index 59128bc..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-name: zookeeper-k8s
-summary: "zookeeper charm for Kubernetes."
-maintainers:
-    - "SolutionsQA <solutionsqa@lists.canonical.com>"
-description: |
-  A CAAS charm to deploy zookeeper.
-tags:
-    - "application"
-series:
-    - kubernetes
-provides:
-    zookeeper:
-        interface: zookeeper
-storage:
-    database:
-        type: filesystem
-        location: /var/lib/zookeeper
-deployment:
-    type: stateful
-    service: cluster
diff --git a/installers/charm/zookeeper-k8s/reactive/spec_template.yaml b/installers/charm/zookeeper-k8s/reactive/spec_template.yaml
deleted file mode 100644 (file)
index 2dd450a..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-version: 2
-containers:
-  - name: %(name)s
-    image: %(docker_image_path)s
-    kubernetes:
-      readinessProbe:
-        tcpSocket:
-          port: %(client-port)s
-        initialDelaySeconds: 10
-        timeoutSeconds: 5
-        failureThreshold: 6
-        successThreshold: 1
-      livenessProbe:
-        tcpSocket:
-          port: %(client-port)s
-        initialDelaySeconds: 20
-    ports:
-    - containerPort: %(client-port)s
-      name: client
-    - containerPort: %(server-port)s
-      name: server
-    - containerPort: %(leader-election-port)s
-      name: leader-election
-    config:
-        ALLOW_ANONYMOUS_LOGIN: 'yes'
-    command:
-    - sh
-    - -c
-    - "start-zookeeper \
-      --servers=%(zookeeper-units)s \
-      --data_dir=/var/lib/zookeeper/data \
-      --data_log_dir=/var/lib/zookeeper/data/log \
-      --conf_dir=/opt/zookeeper/conf \
-      --client_port=%(client-port)s \
-      --election_port=%(leader-election-port)s \
-      --server_port=%(server-port)s \
-      --tick_time=2000 \
-      --init_limit=10 \
-      --sync_limit=5 \
-      --heap=512M \
-      --max_client_cnxns=60 \
-      --snap_retain_count=3 \
-      --purge_interval=12 \
-      --max_session_timeout=40000 \
-      --min_session_timeout=4000 \
-      --log_level=INFO"
-    # readinessProbe:
-    #   exec:
-    #     command:
-    #     - sh
-    #     - -c
-    #     - "zookeeper-ready 2181"
-    #   initialDelaySeconds: 10
-    #   timeoutSeconds: 5
-    #   failureThreshold: 6
-    #   successThreshold: 1
-    # livenessProbe:
-    #   exec:
-    #     command:
-    #     - sh
-    #     - -c
-    #     - "zookeeper-ready 2181"
-    #   initialDelaySeconds: 20
diff --git a/installers/charm/zookeeper-k8s/reactive/zookeeper.py b/installers/charm/zookeeper-k8s/reactive/zookeeper.py
deleted file mode 100644 (file)
index 198e207..0000000
+++ /dev/null
@@ -1,109 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-from charms import layer
-from charms.layer.caas_base import pod_spec_set
-from charms.reactive import endpoint_from_flag
-from charms.reactive import when, when_not, hook
-from charms.reactive.flags import set_flag, clear_flag
-from charmhelpers.core.hookenv import (
-    log,
-    metadata,
-    config,
-)
-
-from charms.osm.k8s import is_pod_up, get_service_ip
-
-
-@hook("upgrade-charm")
-@when("leadership.is_leader")
-def upgrade():
-    clear_flag("zookeeper-k8s.configured")
-
-
-@when("config.changed")
-@when("leadership.is_leader")
-def config_changed():
-    clear_flag("zookeeper-k8s.configured")
-
-
-@when_not("zookeeper-k8s.configured")
-@when("leadership.is_leader")
-def configure():
-    layer.status.maintenance("Configuring zookeeper-k8s container")
-    try:
-        spec = make_pod_spec()
-        log("set pod spec:\n{}".format(spec))
-        pod_spec_set(spec)
-        set_flag("zookeeper-k8s.configured")
-
-    except Exception as e:
-        layer.status.blocked("k8s spec failed to deploy: {}".format(e))
-
-
-@when("zookeeper-k8s.configured")
-def non_leader():
-    layer.status.active("ready")
-
-
-@when_not("leadership.is_leader")
-def non_leaders_active():
-    layer.status.active("ready")
-
-
-@when("zookeeper.joined")
-@when("zookeeper-k8s.configured")
-def send_config():
-    layer.status.maintenance("Sending Zookeeper configuration")
-    if not is_pod_up("zookeeper"):
-        log("The pod is not ready.")
-        return
-
-    zookeeper = endpoint_from_flag("zookeeper.joined")
-    if zookeeper:
-        service_ip = get_service_ip("zookeeper")
-        if service_ip:
-            zookeeper.send_connection(
-                get_zookeeper_client_port(), get_zookeeper_client_port(), service_ip,
-            )
-            layer.status.active("ready")
-
-
-def make_pod_spec():
-    """Make pod specification for Kubernetes
-
-    Returns:
-        pod_spec: Pod specification for Kubernetes
-    """
-    with open("reactive/spec_template.yaml") as spec_file:
-        pod_spec_template = spec_file.read()
-
-    md = metadata()
-    cfg = config()
-    data = {"name": md.get("name"), "docker_image_path": cfg.get("image")}
-    data.update(cfg)
-    return pod_spec_template % data
-
-
-def get_zookeeper_client_port():
-    """Returns Zookeeper port"""
-    cfg = config()
-    return cfg.get("client-port")
diff --git a/installers/charm/zookeeper-k8s/test-requirements.txt b/installers/charm/zookeeper-k8s/test-requirements.txt
deleted file mode 100644 (file)
index 25bd2f9..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-git+https://github.com/davigar15/zaza.git#egg=zaza
-git+https://github.com/python-zk/kazoo
diff --git a/installers/charm/zookeeper-k8s/tests/basic_deployment.py b/installers/charm/zookeeper-k8s/tests/basic_deployment.py
deleted file mode 100644 (file)
index f24112e..0000000
+++ /dev/null
@@ -1,118 +0,0 @@
-#!/usr/bin/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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-import unittest
-import zaza.model as model
-from kazoo.client import KazooClient
-
-
-def get_zookeeper_uri():
-    zookeeper_uri = ""
-    zookeeper_units = model.get_status().applications["zookeeper-k8s"]["units"]
-    for i, unit_name in enumerate(zookeeper_units.keys()):
-        if i:
-            zookeeper_uri += ","
-        unit_ip = zookeeper_units[unit_name]["address"]
-        unit_port = 2181
-        zookeeper_uri += "{}:{}".format(unit_ip, unit_port)
-
-    return zookeeper_uri
-
-
-class BasicDeployment(unittest.TestCase):
-    def test_get_zookeeper_uri(self):
-        get_zookeeper_uri()
-
-    def test_zookeeper_connection(self):
-        zookeeper_uri = get_zookeeper_uri()
-        zk = KazooClient(zookeeper_uri)
-        self.assertEqual(zk.state, "LOST")
-        zk.start()
-        self.assertEqual(zk.state, "CONNECTED")
-        zk.stop()
-        self.assertEqual(zk.state, "LOST")
-
-    def test_zookeeper_create_node(self):
-        zookeeper_uri = get_zookeeper_uri()
-        zk = KazooClient(hosts=zookeeper_uri, read_only=True)
-        zk.start()
-
-        zk.ensure_path("/create/new")
-        self.assertTrue(zk.exists("/create/new"))
-
-        zk.create("/create/new/node", b"a value")
-        self.assertTrue(zk.exists("/create/new/node"))
-
-        zk.stop()
-
-    def test_zookeeper_reading_data(self):
-        zookeeper_uri = get_zookeeper_uri()
-        zk = KazooClient(hosts=zookeeper_uri, read_only=True)
-        zk.start()
-
-        zk.ensure_path("/reading/data")
-        zk.create("/reading/data/node", b"a value")
-
-        data, stat = zk.get("/reading/data")
-        self.assertEqual(data.decode("utf-8"), "")
-
-        children = zk.get_children("/reading/data")
-        self.assertEqual(len(children), 1)
-        self.assertEqual("node", children[0])
-
-        data, stat = zk.get("/reading/data/node")
-        self.assertEqual(data.decode("utf-8"), "a value")
-        zk.stop()
-
-    def test_zookeeper_updating_data(self):
-        zookeeper_uri = get_zookeeper_uri()
-        zk = KazooClient(hosts=zookeeper_uri, read_only=True)
-        zk.start()
-
-        zk.ensure_path("/updating/data")
-        zk.create("/updating/data/node", b"a value")
-
-        data, stat = zk.get("/updating/data/node")
-        self.assertEqual(data.decode("utf-8"), "a value")
-
-        zk.set("/updating/data/node", b"b value")
-        data, stat = zk.get("/updating/data/node")
-        self.assertEqual(data.decode("utf-8"), "b value")
-        zk.stop()
-
-    def test_zookeeper_deleting_data(self):
-        zookeeper_uri = get_zookeeper_uri()
-        zk = KazooClient(hosts=zookeeper_uri, read_only=True)
-        zk.start()
-
-        zk.ensure_path("/deleting/data")
-        zk.create("/deleting/data/node", b"a value")
-
-        zk.delete("/deleting/data/node", recursive=True)
-
-        self.assertFalse(zk.exists("/deleting/data/node"))
-        self.assertTrue(zk.exists("/deleting/data"))
-        data, stat = zk.get("/deleting/data")
-        self.assertEqual(stat.numChildren, 0)
-        zk.delete("/deleting", recursive=True)
-        self.assertFalse(zk.exists("/deleting"))
-        zk.stop()
diff --git a/installers/charm/zookeeper-k8s/tests/bundles/zookeeper-ha.yaml b/installers/charm/zookeeper-k8s/tests/bundles/zookeeper-ha.yaml
deleted file mode 100644 (file)
index 9c893b4..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-bundle: kubernetes
-applications:
-  zookeeper-k8s:
-    charm: '../../release/'
-    scale: 2
-    options:
-      zookeeper-units: 2
-    series: kubernetes
-    storage:
-      database: 50M
diff --git a/installers/charm/zookeeper-k8s/tests/bundles/zookeeper.yaml b/installers/charm/zookeeper-k8s/tests/bundles/zookeeper.yaml
deleted file mode 100644 (file)
index 133606b..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-bundle: kubernetes
-applications:
-  zookeeper-k8s:
-    charm: '../../release/'
-    scale: 1
-    options:
-      zookeeper-units: 1
-    series: kubernetes
-    storage:
-      database: 50M
diff --git a/installers/charm/zookeeper-k8s/tests/tests.yaml b/installers/charm/zookeeper-k8s/tests/tests.yaml
deleted file mode 100644 (file)
index 50a0b09..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-gate_bundles:
-  - zookeeper
-  - zookeeper-ha
-smoke_bundles:
-  - zookeeper
-tests:
-  - tests.basic_deployment.BasicDeployment
diff --git a/installers/charm/zookeeper-k8s/tox.ini b/installers/charm/zookeeper-k8s/tox.ini
deleted file mode 100644 (file)
index 7660519..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-[tox]
-envlist = pep8
-skipsdist = True
-
-[testenv]
-setenv = VIRTUAL_ENV={envdir}
-         PYTHONHASHSEED=0
-whitelist_externals = juju
-passenv = HOME TERM CS_API_* OS_* AMULET_*
-deps = -r{toxinidir}/test-requirements.txt
-install_command =
-  pip install {opts} {packages}
-
-[testenv:build]
-basepython = python3
-passenv=HTTP_PROXY HTTPS_PROXY NO_PROXY
-setenv = CHARM_LAYERS_DIR = /tmp
-whitelist_externals = git
-                      charm
-                      rm
-                      mv
-commands =
-    rm -rf /tmp/canonical-osm /tmp/osm-common
-    rm -rf release
-    git clone https://git.launchpad.net/charm-osm-common /tmp/osm-common
-    charm build . --build-dir /tmp
-    mv /tmp/zookeeper-k8s/ release/
-
-[testenv:black]
-basepython = python3
-deps =
-    black
-    yamllint
-    flake8
-commands =
-    black --check --diff .
-    yamllint .
-    flake8 reactive/ --max-line-length=88
-    flake8 tests/ --max-line-length=88
-
-[testenv:pep8]
-basepython = python3
-deps=charm-tools
-commands = charm-proof
-
-[testenv:func-noop]
-basepython = python3
-commands =
-    true
-
-[testenv:func]
-basepython = python3
-commands = functest-run-suite
-
-[testenv:func-smoke]
-basepython = python3
-commands = functest-run-suite --keep-model --smoke
-
-[testenv:venv]
-commands = {posargs}
diff --git a/installers/charmed_install.sh b/installers/charmed_install.sh
deleted file mode 100755 (executable)
index 21f522d..0000000
+++ /dev/null
@@ -1,594 +0,0 @@
-#! /bin/bash
-#
-#   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.
-#
-
-# set -eux
-
-LXD_VERSION=5.0
-JUJU_VERSION=2.9
-JUJU_AGENT_VERSION=2.9.43
-K8S_CLOUD_NAME="k8s-cloud"
-KUBECTL="microk8s.kubectl"
-MICROK8S_VERSION=1.26
-OSMCLIENT_VERSION=latest
-IMAGES_OVERLAY_FILE=~/.osm/images-overlay.yaml
-PASSWORD_OVERLAY_FILE=~/.osm/password-overlay.yaml
-PATH=/snap/bin:${PATH}
-OSM_DEVOPS="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. &> /dev/null && pwd )"
-INSTALL_PLA=""
-PLA_OVERLAY_FILE=~/.osm/pla-overlay.yaml
-
-if [ -f ${OSM_DEVOPS}/common/all_funcs ] ; then
-    source ${OSM_DEVOPS}/common/all_funcs
-else
-    function track(){
-        true
-    }
-    function FATAL_TRACK(){
-        exit 1
-    }
-fi
-
-MODEL_NAME=osm
-
-OSM_BUNDLE=ch:osm
-OSM_HA_BUNDLE=ch:osm-ha
-CHARMHUB_CHANNEL=latest/beta
-unset TAG
-
-function check_arguments(){
-    while [ $# -gt 0 ] ; do
-        case $1 in
-            --bundle) BUNDLE="$2" ;;
-            --overlay) OVERLAY="$2" ;;
-            --k8s) KUBECFG="$2" ;;
-            --vca) CONTROLLER="$2" ;;
-            --small-profile) INSTALL_NOLXD=y;;
-            --lxd) LXD_CLOUD="$2" ;;
-            --lxd-cred) LXD_CREDENTIALS="$2" ;;
-            --microstack) MICROSTACK=y ;;
-            --ha) BUNDLE=$OSM_HA_BUNDLE ;;
-            --tag) TAG="$2" ;;
-            --registry) REGISTRY_INFO="$2" ;;
-            --only-vca) ONLY_VCA=y ;;
-            --pla) INSTALL_PLA=y ;;
-        esac
-        shift
-    done
-
-    # echo $BUNDLE $KUBECONFIG $LXDENDPOINT
-}
-
-function install_snaps(){
-    if [ ! -v KUBECFG ]; then
-        KUBEGRP="microk8s"
-        sudo snap install microk8s --classic --channel=${MICROK8S_VERSION}/stable ||
-          FATAL_TRACK k8scluster "snap install microk8s ${MICROK8S_VERSION}/stable failed"
-        sudo usermod -a -G microk8s `whoami`
-        # Workaround bug in calico MTU detection
-        if [ ${DEFAULT_IF_MTU} -ne 1500 ] ; then
-            sudo mkdir -p /var/lib/calico
-            sudo ln -sf /var/snap/microk8s/current/var/lib/calico/mtu /var/lib/calico/mtu
-        fi
-        sudo cat /var/snap/microk8s/current/args/kube-apiserver | grep advertise-address || (
-                echo "--advertise-address $DEFAULT_IP" | sudo tee -a /var/snap/microk8s/current/args/kube-apiserver
-                sg ${KUBEGRP} -c microk8s.stop
-                sg ${KUBEGRP} -c microk8s.start
-            )
-        mkdir -p ~/.kube
-        sudo chown -f -R `whoami` ~/.kube
-        sg ${KUBEGRP} -c "microk8s status --wait-ready"
-        KUBECONFIG=~/.osm/microk8s-config.yaml
-        sg ${KUBEGRP} -c "microk8s config" | tee ${KUBECONFIG}
-        track k8scluster k8scluster_ok
-    else
-        KUBECTL="kubectl"
-        sudo snap install kubectl --classic
-        export KUBECONFIG=${KUBECFG}
-        KUBEGRP=$(id -g -n)
-    fi
-    sudo snap install juju --classic --channel=$JUJU_VERSION/stable ||
-    FATAL_TRACK juju "snap install juju ${JUJU_VERSION}/stable failed"
-    track juju juju_ok
-}
-
-function bootstrap_k8s_lxd(){
-    [ -v CONTROLLER ] && ADD_K8S_OPTS="--controller ${CONTROLLER}" && CONTROLLER_NAME=$CONTROLLER
-    [ ! -v CONTROLLER ] && ADD_K8S_OPTS="--client" && BOOTSTRAP_NEEDED="yes" && CONTROLLER_NAME="osm-vca"
-
-    if [ -v BOOTSTRAP_NEEDED ]; then
-        CONTROLLER_PRESENT=$(juju controllers 2>/dev/null| grep ${CONTROLLER_NAME} | wc -l)
-        if [ $CONTROLLER_PRESENT -ge 1 ]; then
-            cat << EOF
-Threre is already a VCA present with the installer reserved name of "${CONTROLLER_NAME}".
-You may either explicitly use this VCA with the "--vca ${CONTROLLER_NAME}" option, or remove it
-using this command:
-
-   juju destroy-controller --release-storage --destroy-all-models -y ${CONTROLLER_NAME}
-
-Please retry the installation once this conflict has been resolved.
-EOF
-            FATAL_TRACK bootstrap_k8s "VCA already present"
-        fi
-    else
-        CONTROLLER_PRESENT=$(juju controllers 2>/dev/null| grep ${CONTROLLER_NAME} | wc -l)
-        if [ $CONTROLLER_PRESENT -le 0 ]; then
-            cat << EOF
-Threre is no VCA present with the name "${CONTROLLER_NAME}".  Please specify a VCA
-that exists, or remove the --vca ${CONTROLLER_NAME} option.
-
-Please retry the installation with one of the solutions applied.
-EOF
-            FATAL_TRACK bootstrap_k8s "Requested VCA not present"
-        fi
-    fi
-
-    if [ -v KUBECFG ]; then
-        cat $KUBECFG | juju add-k8s $K8S_CLOUD_NAME $ADD_K8S_OPTS
-        [ -v BOOTSTRAP_NEEDED ] && juju bootstrap $K8S_CLOUD_NAME $CONTROLLER_NAME \
-            --config controller-service-type=loadbalancer \
-            --agent-version=$JUJU_AGENT_VERSION
-    else
-        sg ${KUBEGRP} -c "echo ${DEFAULT_IP}-${DEFAULT_IP} | microk8s.enable metallb"
-        sg ${KUBEGRP} -c "microk8s.enable ingress"
-        sg ${KUBEGRP} -c "microk8s.enable hostpath-storage dns"
-        TIME_TO_WAIT=30
-        start_time="$(date -u +%s)"
-        while true
-        do
-            now="$(date -u +%s)"
-            if [[ $(( now - start_time )) -gt $TIME_TO_WAIT ]];then
-                echo "Microk8s storage failed to enable"
-                sg ${KUBEGRP} -c "microk8s.status"
-                FATAL_TRACK bootstrap_k8s "Microk8s storage failed to enable"
-            fi
-            storage_status=`sg ${KUBEGRP} -c "microk8s.status -a storage"`
-            if [[ $storage_status == "enabled" ]]; then
-                break
-            fi
-            sleep 1
-        done
-
-        [ ! -v BOOTSTRAP_NEEDED ] && sg ${KUBEGRP} -c "microk8s.config" | juju add-k8s $K8S_CLOUD_NAME $ADD_K8S_OPTS
-        [ -v BOOTSTRAP_NEEDED ] && sg ${KUBEGRP} -c \
-            "juju bootstrap microk8s $CONTROLLER_NAME --config controller-service-type=loadbalancer --agent-version=$JUJU_AGENT_VERSION" \
-            && K8S_CLOUD_NAME=microk8s
-    fi
-    track bootstrap_k8s bootstrap_k8s_ok
-
-    if [ ! -v INSTALL_NOLXD ]; then
-          if [ -v LXD_CLOUD ]; then
-              if [ ! -v LXD_CREDENTIALS ]; then
-                  echo "The installer needs the LXD server certificate if the LXD is external"
-                  FATAL_TRACK bootstrap_lxd "No LXD certificate supplied"
-              fi
-          else
-              LXDENDPOINT=$DEFAULT_IP
-              LXD_CLOUD=~/.osm/lxd-cloud.yaml
-              LXD_CREDENTIALS=~/.osm/lxd-credentials.yaml
-              # Apply sysctl production values for optimal performance
-              sudo cp /usr/share/osm-devops/installers/lxd/60-lxd-production.conf /etc/sysctl.d/60-lxd-production.conf
-              sudo sysctl --system
-              # Install LXD snap
-              sudo apt-get remove --purge -y liblxc1 lxc-common lxcfs lxd lxd-client
-              snap info lxd | grep installed > /dev/null
-              if [ $? -eq 0 ]; then
-                sudo snap refresh lxd --channel $LXD_VERSION/stable
-              else
-                sudo snap install lxd --channel $LXD_VERSION/stable
-              fi
-              # Configure LXD
-              sudo usermod -a -G lxd `whoami`
-              cat /usr/share/osm-devops/installers/lxd/lxd-preseed.conf | sed 's/^config: {}/config:\n  core.https_address: '$LXDENDPOINT':8443/' | sg lxd -c "lxd init --preseed"
-              sg lxd -c "lxd waitready"
-
-              cat << EOF > $LXD_CLOUD
-clouds:
-  lxd-cloud:
-    type: lxd
-    auth-types: [certificate]
-    endpoint: "https://$LXDENDPOINT:8443"
-    config:
-      ssl-hostname-verification: false
-EOF
-              openssl req -nodes -new -x509 -keyout ~/.osm/client.key -out ~/.osm/client.crt -days 365 -subj "/C=FR/ST=Nice/L=Nice/O=ETSI/OU=OSM/CN=osm.etsi.org"
-              cat << EOF > $LXD_CREDENTIALS
-credentials:
-  lxd-cloud:
-    lxd-cloud:
-      auth-type: certificate
-      server-cert: /var/snap/lxd/common/lxd/server.crt
-      client-cert: ~/.osm/client.crt
-      client-key: ~/.osm/client.key
-EOF
-              lxc config trust add local: ~/.osm/client.crt
-          fi
-
-          juju add-cloud -c $CONTROLLER_NAME lxd-cloud $LXD_CLOUD --force
-          juju add-credential -c $CONTROLLER_NAME lxd-cloud -f $LXD_CREDENTIALS
-          sg lxd -c "lxd waitready"
-          juju controller-config features=[k8s-operators]
-          track bootstrap_lxd bootstrap_lxd_ok
-    fi
-}
-
-function deploy_charmed_osm(){
-    if [ -v REGISTRY_INFO ] ; then
-        registry_parts=(${REGISTRY_INFO//@/ })
-        if [ ${#registry_parts[@]} -eq 1 ] ; then
-            # No credentials supplied
-            REGISTRY_USERNAME=""
-            REGISTRY_PASSWORD=""
-            REGISTRY_URL=${registry_parts[0]}
-        else
-            credentials=${registry_parts[0]}
-            credential_parts=(${credentials//:/ })
-            REGISTRY_USERNAME=${credential_parts[0]}
-            REGISTRY_PASSWORD=${credential_parts[1]}
-            REGISTRY_URL=${registry_parts[1]}
-        fi
-        # Ensure the URL ends with a /
-        case $REGISTRY_URL in
-            */) ;;
-            *) REGISTRY_URL=${REGISTRY_URL}/
-        esac
-    fi
-
-    echo "Creating OSM model"
-    if [ -v KUBECFG ]; then
-        juju add-model $MODEL_NAME $K8S_CLOUD_NAME
-    else
-        sg ${KUBEGRP} -c "juju add-model $MODEL_NAME $K8S_CLOUD_NAME"
-    fi
-    echo "Deploying OSM with charms"
-    images_overlay=""
-    if [ -v REGISTRY_URL ]; then
-       [ ! -v TAG ] && TAG='latest'
-    fi
-    [ -v TAG ] && generate_images_overlay && images_overlay="--overlay $IMAGES_OVERLAY_FILE"
-
-    if [ -v OVERLAY ]; then
-        extra_overlay="--overlay $OVERLAY"
-    fi
-    echo "Creating Password Overlay"
-
-    generate_password_overlay && secret_overlay="--overlay $PASSWORD_OVERLAY_FILE"
-
-    [ -n "$INSTALL_PLA" ] && create_pla_overlay && pla_overlay="--overlay $PLA_OVERLAY_FILE"
-
-    if [ -v BUNDLE ]; then
-        juju deploy --trust --channel $CHARMHUB_CHANNEL -m $MODEL_NAME $BUNDLE $images_overlay $extra_overlay $secret_overlay $pla_overlay
-    else
-        juju deploy --trust --channel $CHARMHUB_CHANNEL -m $MODEL_NAME $OSM_BUNDLE $images_overlay $extra_overlay $secret_overlay $pla_overlay
-    fi
-
-    if [ ! -v KUBECFG ]; then
-        API_SERVER=${DEFAULT_IP}
-    else
-        API_SERVER=$(kubectl config view --minify | grep server | cut -f 2- -d ":" | tr -d " ")
-        proto="$(echo $API_SERVER | grep :// | sed -e's,^\(.*://\).*,\1,g')"
-        url="$(echo ${API_SERVER/$proto/})"
-        user="$(echo $url | grep @ | cut -d@ -f1)"
-        hostport="$(echo ${url/$user@/} | cut -d/ -f1)"
-        API_SERVER="$(echo $hostport | sed -e 's,:.*,,g')"
-    fi
-    # Configure VCA Integrator
-    if [ ! -v INSTALL_NOLXD ]; then
-        juju config vca \
-          k8s-cloud=microk8s \
-          lxd-cloud=lxd-cloud:lxd-cloud \
-          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`"
-    else
-        juju config vca \
-          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`"
-    fi
-    # Expose OSM services
-    juju config -m $MODEL_NAME nbi external-hostname=nbi.${API_SERVER}.nip.io
-    juju config -m $MODEL_NAME ng-ui external-hostname=ui.${API_SERVER}.nip.io
-    juju config -m $MODEL_NAME grafana site_url=https://grafana.${API_SERVER}.nip.io
-    juju config -m $MODEL_NAME prometheus site_url=https://prometheus.${API_SERVER}.nip.io
-
-    echo "Waiting for deployment to finish..."
-    check_osm_deployed
-    grafana_leader=`juju status -m $MODEL_NAME grafana | grep "*" | cut -d "*" -f 1`
-    grafana_admin_password=`juju run -m $MODEL_NAME --unit $grafana_leader "echo \\$GF_SECURITY_ADMIN_PASSWORD"`
-    juju config -m $MODEL_NAME mon grafana-password=$grafana_admin_password
-    check_osm_deployed
-    echo "OSM with charms deployed"
-}
-
-function check_osm_deployed() {
-    TIME_TO_WAIT=600
-    start_time="$(date -u +%s)"
-    total_service_count=15
-    [ -n "$INSTALL_PLA" ] && total_service_count=$((total_service_count + 1))
-    previous_count=0
-    while true
-    do
-        service_count=$(juju status --format json -m $MODEL_NAME | jq '.applications[]."application-status".current' | grep active | wc -l)
-        echo "$service_count / $total_service_count services active"
-        if [ $service_count -eq $total_service_count ]; then
-            break
-        fi
-        if [ $service_count -ne $previous_count ]; then
-            previous_count=$service_count
-            start_time="$(date -u +%s)"
-        fi
-        now="$(date -u +%s)"
-        if [[ $(( now - start_time )) -gt $TIME_TO_WAIT ]];then
-            echo "Timed out waiting for OSM services to become ready"
-            FATAL_TRACK deploy_osm "Timed out waiting for services to become ready"
-        fi
-        sleep 10
-    done
-}
-
-function generate_password_overlay() {
-    # prometheus
-    web_config_password=`openssl rand -hex 16`
-    # keystone
-    keystone_db_password=`openssl rand -hex 16`
-    keystone_admin_password=`openssl rand -hex 16`
-    keystone_service_password=`openssl rand -hex 16`
-    #  mariadb
-    mariadb_password=`openssl rand -hex 16`
-    mariadb_root_password=`openssl rand -hex 16`
-    cat << EOF > /tmp/password-overlay.yaml
-applications:
-  prometheus:
-    options:
-      web_config_password: $web_config_password
-  keystone:
-    options:
-      keystone-db-password: $keystone_db_password
-      admin-password: $keystone_admin_password
-      service-password: $keystone_service_password
-  mariadb:
-    options:
-      password: $mariadb_password
-      root_password: $mariadb_root_password
-EOF
-    mv /tmp/password-overlay.yaml $PASSWORD_OVERLAY_FILE
-}
-
-function create_pla_overlay(){
-    echo "Creating PLA Overlay"
-    [ $BUNDLE == $OSM_HA_BUNDLE ] && scale=3 || scale=1
-    cat << EOF > /tmp/pla-overlay.yaml
-applications:
-  pla:
-    charm: osm-pla
-    channel: latest/stable
-    scale: $scale
-    series: kubernetes
-    options:
-      log_level: DEBUG
-    resources:
-      image: opensourcemano/pla:testing-daily
-relations:
-  - - pla:kafka
-    - kafka:kafka
-  - - pla:mongodb
-    - mongodb:database
-EOF
-     mv /tmp/pla-overlay.yaml $PLA_OVERLAY_FILE
-}
-
-function generate_images_overlay(){
-    echo "applications:" > /tmp/images-overlay.yaml
-
-    charms_with_resources="nbi lcm mon pol ng-ui ro"
-    [ -n "$INSTALL_PLA" ] && charms_with_resources+=" pla"
-    for charm in $charms_with_resources; do
-        cat << EOF > /tmp/${charm}_registry.yaml
-registrypath: ${REGISTRY_URL}opensourcemano/${charm}:$TAG
-EOF
-        if [ ! -z "$REGISTRY_USERNAME" ] ; then
-            echo username: $REGISTRY_USERNAME >> /tmp/${charm}_registry.yaml
-            echo password: $REGISTRY_PASSWORD >> /tmp/${charm}_registry.yaml
-        fi
-
-        cat << EOF >> /tmp/images-overlay.yaml
-  ${charm}:
-    resources:
-      ${charm}-image: /tmp/${charm}_registry.yaml
-
-EOF
-    done
-    ch_charms_with_resources="keystone"
-    for charm in $ch_charms_with_resources; do
-        cat << EOF > /tmp/${charm}_registry.yaml
-registrypath: ${REGISTRY_URL}opensourcemano/${charm}:$TAG
-EOF
-        if [ ! -z "$REGISTRY_USERNAME" ] ; then
-            echo username: $REGISTRY_USERNAME >> /tmp/${charm}_registry.yaml
-            echo password: $REGISTRY_PASSWORD >> /tmp/${charm}_registry.yaml
-        fi
-
-        cat << EOF >> /tmp/images-overlay.yaml
-  ${charm}:
-    resources:
-      ${charm}-image: /tmp/${charm}_registry.yaml
-
-EOF
-    done
-
-    mv /tmp/images-overlay.yaml $IMAGES_OVERLAY_FILE
-}
-
-function refresh_osmclient_snap() {
-    osmclient_snap_install_refresh refresh
-}
-
-function install_osm_client_snap() {
-    osmclient_snap_install_refresh install
-}
-
-function osmclient_snap_install_refresh() {
-    channel_preference="stable candidate beta edge"
-    for channel in $channel_preference; do
-        echo "Trying to install osmclient from channel $OSMCLIENT_VERSION/$channel"
-        sudo snap $1 osmclient --channel $OSMCLIENT_VERSION/$channel 2> /dev/null && echo osmclient snap installed && break
-    done
-}
-function install_osmclient() {
-    snap info osmclient | grep -E ^installed: && refresh_osmclient_snap || install_osm_client_snap
-}
-
-function add_local_k8scluster() {
-    osm --all-projects vim-create \
-      --name _system-osm-vim \
-      --account_type dummy \
-      --auth_url http://dummy \
-      --user osm --password osm --tenant osm \
-      --description "dummy" \
-      --config '{management_network_name: mgmt}'
-    tmpfile=$(mktemp --tmpdir=${HOME})
-    cp ${KUBECONFIG} ${tmpfile}
-    osm --all-projects k8scluster-add \
-      --creds ${tmpfile} \
-      --vim _system-osm-vim \
-      --k8s-nets '{"net1": null}' \
-      --version '1.19' \
-      --description "OSM Internal Cluster" \
-      _system-osm-k8s
-    rm -f ${tmpfile}
-}
-
-function install_microstack() {
-    sudo snap install microstack --beta --devmode
-
-    CHECK=$(microstack.openstack server list)
-    if [ $? -ne 0 ] ; then
-        if [[ $CHECK == *"not initialized"* ]]; then
-            echo "Setting MicroStack dashboard to listen to port 8080"
-            sudo snap set microstack config.network.ports.dashboard=8080
-            echo "Initializing MicroStack.  This can take several minutes"
-            sudo microstack.init --auto --control
-        fi
-    fi
-
-    sudo snap alias microstack.openstack openstack
-
-    echo "Updating default security group in MicroStack to allow all access"
-
-    for i in $(microstack.openstack security group list | awk '/default/{ print $2 }'); do
-        for PROTO in icmp tcp udp ; do
-            echo "  $PROTO ingress"
-            CHECK=$(microstack.openstack security group rule create $i --protocol $PROTO --remote-ip 0.0.0.0/0 2>&1)
-            if [ $? -ne 0 ] ; then
-                if [[ $CHECK != *"409"* ]]; then
-                    echo "Error creating ingress rule for $PROTO"
-                    echo $CHECK
-                fi
-            fi
-        done
-    done
-
-    microstack.openstack network show osm-ext &>/dev/null
-    if [ $? -ne 0 ]; then
-       echo "Creating osm-ext network with router to bridge to MicroStack external network"
-        microstack.openstack network create --enable --no-share osm-ext
-        microstack.openstack subnet create osm-ext-subnet --network osm-ext --dns-nameserver 8.8.8.8 \
-              --subnet-range 172.30.0.0/24
-        microstack.openstack router create external-router
-        microstack.openstack router add subnet external-router osm-ext-subnet
-        microstack.openstack router set --external-gateway external external-router
-    fi
-
-    microstack.openstack image list | grep ubuntu20.04 &> /dev/null
-    if [ $? -ne 0 ] ; then
-        echo "Fetching Ubuntu 20.04 image and upLoading to MicroStack"
-        wget -q -O- https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img \
-            | microstack.openstack image create --public --container-format=bare \
-             --disk-format=qcow2 ubuntu20.04 | grep status
-    fi
-
-    if [ ! -f ~/.ssh/microstack ]; then
-        ssh-keygen -t rsa -N "" -f ~/.ssh/microstack
-        microstack.openstack keypair create --public-key ~/.ssh/microstack.pub microstack
-    fi
-
-    echo "Creating VIM microstack-site in OSM"
-    . /var/snap/microstack/common/etc/microstack.rc
-
-    osm vim-create \
-        --name microstack-site \
-        --user "$OS_USERNAME" \
-        --password "$OS_PASSWORD" \
-        --auth_url "$OS_AUTH_URL" \
-        --tenant "$OS_USERNAME" \
-        --account_type openstack \
-        --config='{use_floating_ip: True,
-                   insecure: True,
-                   keypair: microstack,
-                   management_network_name: osm-ext}'
-}
-
-DEFAULT_IF=`ip route list match 0.0.0.0 | awk '{print $5; exit}'`
-DEFAULT_IP=`ip -o -4 a |grep ${DEFAULT_IF}|awk '{split($4,a,"/"); print a[1]; exit}'`
-DEFAULT_IF_MTU=`ip a show ${DEFAULT_IF} | grep mtu | awk '{print $5}'`
-
-check_arguments $@
-mkdir -p ~/.osm
-install_snaps
-bootstrap_k8s_lxd
-if [ -v ONLY_VCA ]; then
-    HOME=/home/$USER
-    k8scloud=microk8s
-    lxdcloud=lxd-cloud:lxd-cloud
-    controllers="`cat $HOME/.local/share/juju/controllers.yaml`"
-    accounts="`cat $HOME/.local/share/juju/accounts.yaml`"
-    publickey="`cat $HOME/.local/share/juju/ssh/juju_id_rsa.pub`"
-    echo "Use the following command to register the installed VCA to your OSM VCA integrator charm"
-    echo -e "  juju config vca \\\n    k8s-cloud=$k8scloud \\\n    lxd-cloud=$lxdcloud \\\n    controllers=$controllers \\\n    accounts=$accounts \\\n    public-key=$publickey"
-    track deploy_osm deploy_vca_only_ok
-else
-    deploy_charmed_osm
-    track deploy_osm deploy_osm_services_k8s_ok
-    install_osmclient
-    track osmclient osmclient_ok
-    export OSM_HOSTNAME=$(juju config -m $MODEL_NAME nbi external-hostname):443
-    export OSM_PASSWORD=$keystone_admin_password
-    sleep 10
-    add_local_k8scluster
-    track final_ops add_local_k8scluster_ok
-    if [ -v MICROSTACK ]; then
-        install_microstack
-        track final_ops install_microstack_ok
-    fi
-
-    echo "Your installation is now complete, follow these steps for configuring the osmclient:"
-    echo
-    echo "1. Create the OSM_HOSTNAME environment variable with the NBI IP"
-    echo
-    echo "export OSM_HOSTNAME=$OSM_HOSTNAME"
-    echo "export OSM_PASSWORD=$OSM_PASSWORD"
-    echo
-    echo "2. Add the previous commands to your .bashrc for other Shell sessions"
-    echo
-    echo "echo \"export OSM_HOSTNAME=$OSM_HOSTNAME\" >> ~/.bashrc"
-    echo "echo \"export OSM_PASSWORD=$OSM_PASSWORD\" >> ~/.bashrc"
-    echo
-    echo "3. Login OSM GUI by using admin password: $OSM_PASSWORD"
-    echo
-    echo "DONE"
-    track end
-fi
-
diff --git a/installers/charmed_uninstall.sh b/installers/charmed_uninstall.sh
deleted file mode 100755 (executable)
index 386cb04..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-#! /bin/bash
-#
-#   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.
-#
-
-
-juju destroy-model osm --destroy-storage -y
-sudo snap unalias osm
-sudo snap remove osmclient
-CONTROLLER_NAME="osm-vca"
-CONTROLLER_PRESENT=$(juju controllers 2>/dev/null| grep ${CONTROLLER_NAME} | wc -l)
-if [[ $CONTROLLER_PRESENT -ge 1 ]]; then
-    cat << EOF
-The VCA with the name "${CONTROLLER_NAME}" has been left in place to ensure that no other
-applications are using it.  If you are sure you wish to remove this controller,
-please execute the following command:
-
-   juju destroy-controller --release-storage --destroy-all-models -y ${CONTROLLER_NAME}
-
-EOF
-fi
index f0723ea..522d228 100755 (executable)
@@ -21,7 +21,7 @@ function usage(){
     echo -e "     -h / --help:    print this help"
     echo -e "     -y:             do not prompt for confirmation, assumes yes"
     echo -e "     -r <repo>:      use specified repository name for osm packages"
-    echo -e "     -R <release>:   use specified release for osm binaries (deb packages, lxd images, ...)"
+    echo -e "     -R <release>:   use specified release for osm binaries (deb packages, ...)"
     echo -e "     -u <repo base>: use specified repository url for osm packages"
     echo -e "     -k <repo key>:  use specified repository public key url"
     echo -e "     -a <apt proxy url>: use this apt proxy url when downloading apt packages (air-gapped installation)"
@@ -33,92 +33,20 @@ function usage(){
     echo -e "     --no-aux-cluster: Do not provision an auxiliary cluster for cloud-native gitops operations in OSM (NEW in Release SIXTEEN) (by default, it is installed)"
     echo -e "     -D <devops path>:   use local devops installation path"
     echo -e "     -s <namespace>  namespace when installed using k8s, default is osm"
-    echo -e "     -H <VCA host>   use specific juju host controller IP"
-    echo -e "     -S <VCA secret> use VCA/juju secret key"
-    echo -e "     -P <VCA pubkey> use VCA/juju public key file"
-    echo -e "     -A <VCA apiproxy> use VCA/juju API proxy"
     echo -e "     -w <work dir>:   Location to store runtime installation"
-    echo -e "     -l:             LXD cloud yaml file"
-    echo -e "     -L:             LXD credentials yaml file"
     echo -e "     -K:             Specifies the name of the controller to use - The controller must be already bootstrapped"
     echo -e "     -d <docker registry URL> use docker registry URL instead of dockerhub"
     echo -e "     -p <docker proxy URL> set docker proxy URL as part of docker CE configuration"
     echo -e "     -T <docker tag> specify docker tag for the modules specified with option -m"
     echo -e "     --debug:        debug mode"
-    echo -e "     --nocachelxdimages:  do not cache local lxd images, do not create cronjob for that cache (will save installation time, might affect instantiation time)"
-    echo -e "     --cachelxdimages:  cache local lxd images, create cronjob for that cache (will make installation longer)"
-    echo -e "     --nolxd:        do not install and configure LXD, allowing unattended installations (assumes LXD is already installed and confifured)"
     echo -e "     --nodocker:     do not install docker, do not initialize a swarm (assumes docker is already installed and a swarm has been initialized)"
-    echo -e "     --nojuju:       do not juju, assumes already installed"
-    echo -e "     --nohostports:  do not expose docker ports to host (useful for creating multiple instances of osm on the same host)"
     echo -e "     --nohostclient: do not install the osmclient"
     echo -e "     --uninstall:    uninstall OSM: remove the containers and delete NAT rules"
     echo -e "     --k8s_monitor:  install the OSM kubernetes monitoring with prometheus and grafana"
     echo -e "     --showopts:     print chosen options and exit (only for debugging)"
-    echo -e "     --charmed:                   Deploy and operate OSM with Charms on k8s"
-    echo -e "     [--bundle <bundle path>]:    Specify with which bundle to deploy OSM with charms (--charmed option)"
-    echo -e "     [--k8s <kubeconfig path>]:   Specify with which kubernetes to deploy OSM with charms (--charmed option)"
-    echo -e "     [--vca <name>]:              Specifies the name of the controller to use - The controller must be already bootstrapped (--charmed option)"
-    echo -e "     [--small-profile]:           Do not install and configure LXD which aims to use only K8s Clouds (--charmed option)"
-    echo -e "     [--lxd <yaml path>]:         Takes a YAML file as a parameter with the LXD Cloud information (--charmed option)"
-    echo -e "     [--lxd-cred <yaml path>]:    Takes a YAML file as a parameter with the LXD Credentials information (--charmed option)"
-    echo -e "     [--microstack]:              Installs microstack as a vim. (--charmed option)"
-    echo -e "     [--overlay]:                 Add an overlay to override some defaults of the default bundle (--charmed option)"
-    echo -e "     [--ha]:                      Installs High Availability bundle. (--charmed option)"
-    echo -e "     [--tag]:                     Docker image tag. (--charmed option)"
-    echo -e "     [--registry]:                Docker registry with optional credentials as user:pass@hostname:port (--charmed option)"
     [ -z "${DEBUG_INSTALL}" ] || DEBUG end of function
 }
 
-# takes a juju/accounts.yaml file and returns the password specific
-# for a controller. I wrote this using only bash tools to minimize
-# additions of other packages
-function parse_juju_password {
-    [ -z "${DEBUG_INSTALL}" ] || DEBUG beginning of function
-    password_file="${HOME}/.local/share/juju/accounts.yaml"
-    local controller_name=$1
-    local s='[[:space:]]*' w='[a-zA-Z0-9_-]*' fs=$(echo @|tr @ '\034')
-    sed -ne "s|^\($s\):|\1|" \
-         -e "s|^\($s\)\($w\)$s:$s[\"']\(.*\)[\"']$s\$|\1$fs\2$fs\3|p" \
-         -e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" $password_file |
-    awk -F$fs -v controller=$controller_name '{
-        indent = length($1)/2;
-        vname[indent] = $2;
-        for (i in vname) {if (i > indent) {delete vname[i]}}
-        if (length($3) > 0) {
-            vn=""; for (i=0; i<indent; i++) {vn=(vn)(vname[i])("_")}
-            if (match(vn,controller) && match($2,"password")) {
-                printf("%s",$3);
-            }
-        }
-    }'
-    [ -z "${DEBUG_INSTALL}" ] || DEBUG end of function
-}
-
-function set_vca_variables() {
-    OSM_VCA_CLOUDNAME="lxd-cloud"
-    [ -n "$OSM_VCA_HOST" ] && OSM_VCA_CLOUDNAME="localhost"
-    if [ -z "$OSM_VCA_HOST" ]; then
-        [ -z "$CONTROLLER_NAME" ] && OSM_VCA_HOST=`sg lxd -c "juju show-controller $OSM_NAMESPACE"|grep api-endpoints|awk -F\' '{print $2}'|awk -F\: '{print $1}'`
-        [ -n "$CONTROLLER_NAME" ] && OSM_VCA_HOST=`juju show-controller $CONTROLLER_NAME |grep api-endpoints|awk -F\' '{print $2}'|awk -F\: '{print $1}'`
-        [ -z "$OSM_VCA_HOST" ] && FATAL "Cannot obtain juju controller IP address"
-    fi
-    if [ -z "$OSM_VCA_SECRET" ]; then
-        [ -z "$CONTROLLER_NAME" ] && OSM_VCA_SECRET=$(parse_juju_password $OSM_NAMESPACE)
-        [ -n "$CONTROLLER_NAME" ] && OSM_VCA_SECRET=$(parse_juju_password $CONTROLLER_NAME)
-        [ -z "$OSM_VCA_SECRET" ] && FATAL "Cannot obtain juju secret"
-    fi
-    if [ -z "$OSM_VCA_PUBKEY" ]; then
-        OSM_VCA_PUBKEY=$(cat $HOME/.local/share/juju/ssh/juju_id_rsa.pub)
-        [ -z "$OSM_VCA_PUBKEY" ] && FATAL "Cannot obtain juju public key"
-    fi
-    if [ -z "$OSM_VCA_CACERT" ]; then
-        [ -z "$CONTROLLER_NAME" ] && OSM_VCA_CACERT=$(juju controllers --format json | jq -r --arg controller $OSM_NAMESPACE '.controllers[$controller]["ca-cert"]' | base64 | tr -d \\n)
-        [ -n "$CONTROLLER_NAME" ] && OSM_VCA_CACERT=$(juju controllers --format json | jq -r --arg controller $CONTROLLER_NAME '.controllers[$controller]["ca-cert"]' | base64 | tr -d \\n)
-        [ -z "$OSM_VCA_CACERT" ] && FATAL "Cannot obtain juju CA certificate"
-    fi
-}
-
 function generate_secret() {
     [ -z "${DEBUG_INSTALL}" ] || DEBUG beginning of function
     head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32
@@ -199,12 +127,6 @@ function deploy_osm_helm_chart() {
     [ -z "${DEBUG_INSTALL}" ] || DEBUG beginning of function
     # Generate helm values to be passed with -f osm-values.yaml
     sudo mkdir -p ${OSM_HELM_WORK_DIR}
-    if [ -n "${INSTALL_JUJU}" ]; then
-        sudo bash -c "cat << EOF > ${OSM_HELM_WORK_DIR}/osm-values.yaml
-vca:
-  pubkey: \"${OSM_VCA_PUBKEY}\"
-EOF"
-    fi
 
     # Generate helm values to be passed with --set
     OSM_HELM_OPTS=""
@@ -228,18 +150,6 @@ EOF"
         OSM_HELM_OPTS="${OSM_HELM_OPTS} --set global.gitops.pubkey=${AGE_MGMT_PUBKEY}"
     fi
 
-    if [ -n "${INSTALL_JUJU}" ]; then
-        OSM_HELM_OPTS="${OSM_HELM_OPTS} --set vca.enabled=true"
-        OSM_HELM_OPTS="${OSM_HELM_OPTS} --set vca.host=${OSM_VCA_HOST}"
-        OSM_HELM_OPTS="${OSM_HELM_OPTS} --set vca.secret=${OSM_VCA_SECRET}"
-        OSM_HELM_OPTS="${OSM_HELM_OPTS} --set vca.cacert=${OSM_VCA_CACERT}"
-    fi
-    [ -n "$OSM_VCA_APIPROXY" ] && OSM_HELM_OPTS="${OSM_HELM_OPTS} --set lcm.config.OSMLCM_VCA_APIPROXY=${OSM_VCA_APIPROXY}"
-
-    OSM_HELM_OPTS="${OSM_HELM_OPTS} --set airflow.defaultAirflowRepository=${DOCKER_REGISTRY_URL}${DOCKER_USER}/airflow"
-    [ ! "$OSM_DOCKER_TAG" == "testing-daily" ] && OSM_HELM_OPTS="${OSM_HELM_OPTS} --set-string airflow.defaultAirflowTag=${OSM_DOCKER_TAG}"
-    OSM_HELM_OPTS="${OSM_HELM_OPTS} --set airflow.ingress.web.hosts[0].name=airflow.${OSM_K8S_EXTERNAL_IP}.nip.io"
-
     if [ -n "${OSM_BEHIND_PROXY}" ]; then
         OSM_HELM_OPTS="${OSM_HELM_OPTS} --set global.behindHttpProxy=true"
         [ -n "${HTTP_PROXY}" ] && OSM_HELM_OPTS="${OSM_HELM_OPTS} --set global.httpProxy.HTTP_PROXY=\"${HTTP_PROXY}\""
@@ -255,9 +165,6 @@ EOF"
         fi
     fi
 
-    if [ -n "${INSTALL_JUJU}" ]; then
-        OSM_HELM_OPTS="-f ${OSM_HELM_WORK_DIR}/osm-values.yaml ${OSM_HELM_OPTS}"
-    fi
     echo "helm upgrade --install -n $OSM_NAMESPACE --create-namespace $OSM_NAMESPACE $OSM_DEVOPS/installers/helm/osm ${OSM_HELM_OPTS}"
     helm upgrade --install -n $OSM_NAMESPACE --create-namespace $OSM_NAMESPACE $OSM_DEVOPS/installers/helm/osm ${OSM_HELM_OPTS}
     # Override existing values.yaml with the final values.yaml used to install OSM
@@ -307,11 +214,10 @@ function ask_proceed() {
     [ -z "${DEBUG_INSTALL}" ] || DEBUG beginning of function
 
     [ -z "$ASSUME_YES" ] && ! ask_user "The installation will do the following
-    1. Install and configure LXD
-    2. Install juju
-    3. Install docker CE
-    4. Disable swap space
-    5. Install and initialize Kubernetes
+    1. Install required packages
+    2. Install docker CE
+    3. Disable swap space
+    4. Install and initialize Kubernetes
     as pre-requirements.
     Do you want to proceed (Y/n)? " y && echo "Cancelled!" && exit 1
 
@@ -339,19 +245,19 @@ The following env variables have been found for the current user:
 ${OSM_PROXY_ENV_VARIABLES}.
 
 This suggests that this machine is behind a proxy and a special configuration is required.
-The installer will install Docker CE, LXD and Juju to work behind a proxy using those
+The installer will install Docker CE and a Kubernetes to work behind a proxy using those
 env variables.
 
-Take into account that the installer uses apt, curl, wget, docker, lxd, juju and snap.
+Take into account that the installer uses apt, curl, wget and docker.
 Depending on the program, the env variables to work behind a proxy might be different
 (e.g. http_proxy vs HTTP_PROXY).
 
 For that reason, it is strongly recommended that at least http_proxy, https_proxy, HTTP_PROXY
 and HTTPS_PROXY are defined.
 
-Finally, some of the programs (apt, snap) those programs are run as sudoer, requiring that
-those env variables are also set for root user. If you are not sure whether those variables
-are configured for the root user, you can stop the installation now.
+Finally, some of the programs (apt) are run as sudoer, requiring that those env variables
+are also set for root user. If you are not sure whether those variables are configured for
+the root user, you can stop the installation now.
 
 Do you want to proceed with the installation (Y/n)? " y && echo "Cancelled!" && exit 1
     else
@@ -364,27 +270,14 @@ Do you want to proceed with the installation (Y/n)? " y && echo "Cancelled!" &&
 function find_devops_folder() {
     [ -z "${DEBUG_INSTALL}" ] || DEBUG beginning of function
     if [ -z "$OSM_DEVOPS" ]; then
-        if [ -n "$TEST_INSTALLER" ]; then
-            echo -e "\nUsing local devops repo for OSM installation"
-            OSM_DEVOPS="$(dirname $(realpath $(dirname $0)))"
-        else
-            echo -e "\nCreating temporary dir for OSM installation"
-            OSM_DEVOPS="$(mktemp -d -q --tmpdir "installosm.XXXXXX")"
-            trap 'rm -rf "$OSM_DEVOPS"' EXIT
-            git clone https://osm.etsi.org/gerrit/osm/devops.git $OSM_DEVOPS
-        fi
+        echo -e "\nCreating temporary dir for OSM installation"
+        OSM_DEVOPS="$(mktemp -d -q --tmpdir "installosm.XXXXXX")"
+        trap 'rm -rf "$OSM_DEVOPS"' EXIT
+        git clone https://osm.etsi.org/gerrit/osm/devops.git $OSM_DEVOPS
     fi
     [ -z "${DEBUG_INSTALL}" ] || DEBUG end of function
 }
 
-function install_lxd() {
-    [ -z "${DEBUG_INSTALL}" ] || DEBUG beginning of function
-    LXD_INSTALL_OPTS="-D ${OSM_DEVOPS} -i ${OSM_DEFAULT_IF} ${DEBUG_INSTALL}"
-    [ -n "${OSM_BEHIND_PROXY}" ] && LXD_INSTALL_OPTS="${LXD_INSTALL_OPTS} -P"
-    $OSM_DEVOPS/installers/install_lxd.sh ${LXD_INSTALL_OPTS} || FATAL_TRACK lxd "install_lxd.sh failed"
-    [ -z "${DEBUG_INSTALL}" ] || DEBUG end of function
-}
-
 function install_docker_ce() {
     [ -z "${DEBUG_INSTALL}" ] || DEBUG beginning of function
     DOCKER_CE_OPTS="-D ${OSM_DEVOPS} ${DEBUG_INSTALL}"
@@ -451,10 +344,7 @@ function install_osm() {
     trap ctrl_c INT
 
     check_osm_behind_proxy
-    check_packages "git wget curl tar snapd"
-    if [ -n "${INSTALL_JUJU}" ]; then
-        sudo snap install jq || FATAL "Could not install jq (snap package). Make sure that snap works"
-    fi
+    check_packages "git wget curl tar"
     find_devops_folder
 
     track start release $RELEASE none none docker_tag $OSM_DOCKER_TAG none none installation_type $OSM_INSTALLATION_TYPE none none os_info $os_info none none
@@ -475,9 +365,6 @@ function install_osm() {
     # configure apt proxy
     [ -n "$APT_PROXY_URL" ] && configure_apt_proxy $APT_PROXY_URL
 
-    # if lxd is requested, we will install it
-    [ -n "$INSTALL_LXD" ] && install_lxd
-
     track prereq prereqok_ok
 
     if [ -n "$INSTALL_DOCKER" ] || [ "${K8S_CLUSTER_ENGINE}" == "kubeadm" ]; then
@@ -499,22 +386,6 @@ function install_osm() {
     kubectl create namespace ${OSM_NAMESPACE}
     track k8scluster k8scluster_ok
 
-    if [ -n "${INSTALL_JUJU}" ]; then
-        echo "Installing Juju ..."
-        JUJU_OPTS="-D ${OSM_DEVOPS} -s ${OSM_NAMESPACE} -i ${OSM_DEFAULT_IP} ${DEBUG_INSTALL} ${INSTALL_CACHELXDIMAGES}"
-        [ -n "${OSM_VCA_HOST}" ] && JUJU_OPTS="$JUJU_OPTS -H ${OSM_VCA_HOST}"
-        [ -n "${LXD_CLOUD_FILE}" ] && JUJU_OPTS="$JUJU_OPTS -l ${LXD_CLOUD_FILE}"
-        [ -n "${LXD_CRED_FILE}" ] && JUJU_OPTS="$JUJU_OPTS -L ${LXD_CRED_FILE}"
-        [ -n "${CONTROLLER_NAME}" ] && JUJU_OPTS="$JUJU_OPTS -K ${CONTROLLER_NAME}"
-        [ -n "${OSM_BEHIND_PROXY}" ] && JUJU_OPTS="${JUJU_OPTS} -P"
-        $OSM_DEVOPS/installers/install_juju.sh ${JUJU_OPTS} || FATAL_TRACK juju "install_juju.sh failed"
-        set_vca_variables
-    fi
-    track juju juju_ok
-
-    # This track is maintained for backwards compatibility
-    track docker_images docker_images_ok
-
     # Install mgmt cluster
     echo "Installing mgmt cluster ..."
     MGMTCLUSTER_INSTALL_OPTS="-D ${OSM_DEVOPS} ${DEBUG_INSTALL}"
@@ -557,11 +428,6 @@ function install_osm() {
     add_local_k8scluster
     track final_ops add_local_k8scluster_ok
 
-    # if lxd is requested, iptables firewall is updated to work with both docker and LXD
-    if [ -n "$INSTALL_LXD" ]; then
-        arrange_docker_default_network_policy
-    fi
-
     wget -q -O- https://osm-download.etsi.org/ftp/osm-16.0-sixteen/README2.txt &> /dev/null
     track end
     sudo find /etc/osm
@@ -569,13 +435,6 @@ function install_osm() {
     return 0
 }
 
-function arrange_docker_default_network_policy() {
-    echo -e "Fixing firewall so docker and LXD can share the same host without affecting each other."
-    sudo iptables -I DOCKER-USER -j ACCEPT
-    sudo iptables-save | sudo tee /etc/iptables/rules.v4
-    sudo ip6tables-save | sudo tee /etc/iptables/rules.v6
-}
-
 function install_k8s_monitoring() {
     [ -z "${DEBUG_INSTALL}" ] || DEBUG beginning of function
     # install OSM monitoring
@@ -591,30 +450,21 @@ function dump_vars(){
     echo "DOCKER_PROXY_URL=$DOCKER_PROXY_URL"
     echo "DOCKER_REGISTRY_URL=$DOCKER_REGISTRY_URL"
     echo "DOCKER_USER=$DOCKER_USER"
-    echo "INSTALL_CACHELXDIMAGES=$INSTALL_CACHELXDIMAGES"
-    echo "INSTALL_JUJU=$INSTALL_JUJU"
     echo "INSTALL_K8S_MONITOR=$INSTALL_K8S_MONITOR"
-    echo "INSTALL_LXD=$INSTALL_LXD"
     echo "INSTALL_DOCKER=$INSTALL_DOCKER"
     echo "OSM_DEVOPS=$OSM_DEVOPS"
     echo "OSM_DOCKER_TAG=$OSM_DOCKER_TAG"
     echo "OSM_K8S_EXTERNAL_IP=$OSM_K8S_EXTERNAL_IP"
     echo "OSM_HELM_WORK_DIR=$OSM_HELM_WORK_DIR"
     echo "OSM_NAMESPACE=$OSM_NAMESPACE"
-    echo "OSM_VCA_HOST=$OSM_VCA_HOST"
-    echo "OSM_VCA_PUBKEY=$OSM_VCA_PUBKEY"
-    echo "OSM_VCA_SECRET=$OSM_VCA_SECRET"
     echo "OSM_WORK_DIR=$OSM_WORK_DIR"
     echo "PULL_IMAGES=$PULL_IMAGES"
-    echo "RECONFIGURE=$RECONFIGURE"
     echo "RELEASE=$RELEASE"
     echo "REPOSITORY=$REPOSITORY"
     echo "REPOSITORY_BASE=$REPOSITORY_BASE"
     echo "REPOSITORY_KEY=$REPOSITORY_KEY"
     echo "SHOWOPTS=$SHOWOPTS"
-    echo "TEST_INSTALLER=$TEST_INSTALLER"
     echo "UNINSTALL=$UNINSTALL"
-    echo "UPDATE=$UPDATE"
     [ -z "${DEBUG_INSTALL}" ] || DEBUG end of function
 }
 
@@ -634,10 +484,6 @@ function ctrl_c() {
 }
 
 UNINSTALL=""
-UPDATE=""
-RECONFIGURE=""
-TEST_INSTALLER=""
-INSTALL_LXD=""
 SHOWOPTS=""
 ASSUME_YES=""
 APT_PROXY_URL=""
@@ -646,20 +492,11 @@ DEBUG_INSTALL=""
 RELEASE="testing-daily"
 REPOSITORY="testing"
 INSTALL_K8S_MONITOR=""
-LXD_REPOSITORY_BASE="https://osm-download.etsi.org/repository/osm/lxd"
-LXD_REPOSITORY_PATH=""
 INSTALL_DOCKER=""
-INSTALL_JUJU=""
 INSTALL_NOHOSTCLIENT=""
-INSTALL_CACHELXDIMAGES=""
 INSTALL_AUX_CLUSTER="y"
 INSTALL_MGMT_CLUSTER="y"
 OSM_DEVOPS=
-OSM_VCA_HOST=
-OSM_VCA_SECRET=
-OSM_VCA_PUBKEY=
-OSM_VCA_CLOUDNAME="localhost"
-OSM_VCA_K8S_CLOUDNAME="k8scloud"
 OSM_NAMESPACE=osm
 REPOSITORY_KEY="OSM%20ETSI%20Release%20Key.gpg"
 REPOSITORY_BASE="https://osm-download.etsi.org/repository/osm/debian"
@@ -688,7 +525,7 @@ DOCKER_PROXY_URL=
 MODULE_DOCKER_TAG=
 OSM_INSTALLATION_TYPE="Default"
 
-while getopts ":a:c:e:r:n:k:u:R:D:o:O:N:H:S:s:t:U:P:A:l:L:K:d:p:T:f:F:G:M:-: hy" o; do
+while getopts ":a:c:e:r:n:k:u:R:D:o:O:N:s:t:U:l:L:K:d:p:T:f:F:G:M:-: hy" o; do
     case "${o}" in
         a)
             APT_PROXY_URL=${OPTARG}
@@ -722,12 +559,6 @@ while getopts ":a:c:e:r:n:k:u:R:D:o:O:N:H:S:s:t:U:P:A:l:L:K:d:p:T:f:F:G:M:-: hy"
         D)
             OSM_DEVOPS="${OPTARG}"
             ;;
-        H)
-            OSM_VCA_HOST="${OPTARG}"
-            ;;
-        S)
-            OSM_VCA_SECRET="${OPTARG}"
-            ;;
         s)
             OSM_NAMESPACE="${OPTARG}" && [[ ! "${OPTARG}" =~ $RE_CHECK ]] && echo "Namespace $OPTARG is invalid. Regex used for validation is $RE_CHECK" && exit 0
             ;;
@@ -738,18 +569,6 @@ while getopts ":a:c:e:r:n:k:u:R:D:o:O:N:H:S:s:t:U:P:A:l:L:K:d:p:T:f:F:G:M:-: hy"
         U)
             DOCKER_USER="${OPTARG}"
             ;;
-        P)
-            OSM_VCA_PUBKEY=$(cat ${OPTARG})
-            ;;
-        A)
-            OSM_VCA_APIPROXY="${OPTARG}"
-            ;;
-        l)
-            LXD_CLOUD_FILE="${OPTARG}"
-            ;;
-        L)
-            LXD_CRED_FILE="${OPTARG}"
-            ;;
         K)
             CONTROLLER_NAME="${OPTARG}"
             ;;
@@ -774,33 +593,11 @@ while getopts ":a:c:e:r:n:k:u:R:D:o:O:N:H:S:s:t:U:P:A:l:L:K:d:p:T:f:F:G:M:-: hy"
             [ "${OPTARG}" == "uninstall" ] && UNINSTALL="y" && continue
             [ "${OPTARG}" == "no-mgmt-cluster" ] && INSTALL_MGMT_CLUSTER="" && continue
             [ "${OPTARG}" == "no-aux-cluster" ] && INSTALL_AUX_CLUSTER="" && continue
-            [ "${OPTARG}" == "update" ] && UPDATE="y" && continue
-            [ "${OPTARG}" == "reconfigure" ] && RECONFIGURE="y" && continue
-            [ "${OPTARG}" == "test" ] && TEST_INSTALLER="y" && continue
-            [ "${OPTARG}" == "lxdinstall" ] && INSTALL_LXD="y" && continue
-            [ "${OPTARG}" == "lxd" ] && INSTALL_LXD="y" && continue
-            [ "${OPTARG}" == "nolxd" ] && INSTALL_LXD="" && continue
             [ "${OPTARG}" == "docker" ] && INSTALL_DOCKER="y" && continue
             [ "${OPTARG}" == "nodocker" ] && INSTALL_DOCKER="" && continue
             [ "${OPTARG}" == "showopts" ] && SHOWOPTS="y" && continue
-            [ "${OPTARG}" == "juju" ] && INSTALL_JUJU="y" && continue
-            [ "${OPTARG}" == "nojuju" ] && INSTALL_JUJU="" && continue
             [ "${OPTARG}" == "nohostclient" ] && INSTALL_NOHOSTCLIENT="y" && continue
             [ "${OPTARG}" == "k8s_monitor" ] && INSTALL_K8S_MONITOR="y" && continue
-            [ "${OPTARG}" == "charmed" ] && CHARMED="y" && OSM_INSTALLATION_TYPE="Charmed" && continue
-            [ "${OPTARG}" == "bundle" ] && continue
-            [ "${OPTARG}" == "k8s" ] && continue
-            [ "${OPTARG}" == "lxd-cred" ] && continue
-            [ "${OPTARG}" == "microstack" ] && continue
-            [ "${OPTARG}" == "overlay" ] && continue
-            [ "${OPTARG}" == "only-vca" ] && continue
-            [ "${OPTARG}" == "small-profile" ] && continue
-            [ "${OPTARG}" == "vca" ] && continue
-            [ "${OPTARG}" == "ha" ] && continue
-            [ "${OPTARG}" == "tag" ] && continue
-            [ "${OPTARG}" == "registry" ] && continue
-            [ "${OPTARG}" == "nocachelxdimages" ] && continue
-            [ "${OPTARG}" == "cachelxdimages" ] && INSTALL_CACHELXDIMAGES="--cachelxdimages" && continue
             echo -e "Invalid option: '--$OPTARG'\n" >&2
             usage && exit 1
             ;;
@@ -831,13 +628,8 @@ source $OSM_DEVOPS/common/all_funcs
 
 # Uninstall if "--uninstall"
 if [ -n "$UNINSTALL" ]; then
-    if [ -n "$CHARMED" ]; then
-        ${OSM_DEVOPS}/installers/charmed_uninstall.sh -R $RELEASE -r $REPOSITORY -u $REPOSITORY_BASE -D $OSM_DEVOPS -t $DOCKER_TAG "$@" || \
-        FATAL_TRACK charmed_uninstall "charmed_uninstall.sh failed"
-    else
-        ${OSM_DEVOPS}/installers/uninstall_osm.sh "$@" || \
-        FATAL_TRACK community_uninstall "uninstall_osm.sh failed"
-    fi
+    ${OSM_DEVOPS}/installers/uninstall_osm.sh "$@" || \
+    FATAL_TRACK community_uninstall "uninstall_osm.sh failed"
     echo -e "\nDONE"
     exit 0
 fi
diff --git a/installers/install_juju.sh b/installers/install_juju.sh
deleted file mode 100755 (executable)
index 7be5f99..0000000
+++ /dev/null
@@ -1,283 +0,0 @@
-#!/bin/bash
-#
-#   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.
-#
-
-function usage(){
-    [ -z "${DEBUG_INSTALL}" ] || DEBUG beginning of function
-    echo -e "usage: $0 [OPTIONS]"
-    echo -e "Install Juju for OSM"
-    echo -e "  OPTIONS"
-    echo -e "     -h / --help:    print this help"
-    echo -e "     -D <devops path> use local devops installation path"
-    echo -e "     -s <stack name> or <namespace>  user defined stack name when installed using swarm or namespace when installed using k8s, default is osm"
-    echo -e "     -H <VCA host>   use specific juju host controller IP"
-    echo -e "     -S <VCA secret> use VCA/juju secret key"
-    echo -e "     -P <VCA pubkey> use VCA/juju public key file"
-    echo -e "     -l:             LXD cloud yaml file"
-    echo -e "     -L:             LXD credentials yaml file"
-    echo -e "     -K:             Specifies the name of the controller to use - The controller must be already bootstrapped"
-    echo -e "     --debug:        debug mode"
-    echo -e "     --cachelxdimages:  cache local lxd images, create cronjob for that cache (will make installation longer)"
-    [ -z "${DEBUG_INSTALL}" ] || DEBUG end of function
-}
-
-function update_juju_images(){
-    [ -z "${DEBUG_INSTALL}" ] || DEBUG beginning of function
-    crontab -l | grep update-juju-lxc-images || (crontab -l 2>/dev/null; echo "0 4 * * 6 $USER ${OSM_DEVOPS}/installers/update-juju-lxc-images --xenial --bionic") | crontab -
-    ${OSM_DEVOPS}/installers/update-juju-lxc-images --xenial --bionic
-    [ -z "${DEBUG_INSTALL}" ] || DEBUG end of function
-}
-
-function install_juju_client() {
-    [ -z "${DEBUG_INSTALL}" ] || DEBUG beginning of function
-    echo "Installing juju client"
-    sudo snap install juju --classic --channel=$JUJU_VERSION/stable
-    [[ ":$PATH": != *":/snap/bin:"* ]] && PATH="/snap/bin:${PATH}"
-    [ -n "$INSTALL_CACHELXDIMAGES" ] && update_juju_images
-    echo "Finished installation of juju client"
-    [ -z "${DEBUG_INSTALL}" ] || DEBUG end of function
-    return 0
-}
-
-function juju_createcontroller_k8s(){
-    [ -z "${DEBUG_INSTALL}" ] || DEBUG beginning of function
-    cat $HOME/.kube/config | juju add-k8s $OSM_VCA_K8S_CLOUDNAME --client \
-    || FATAL_TRACK juju "Failed to add K8s endpoint and credential for client in cloud $OSM_VCA_K8S_CLOUDNAME"
-
-    JUJU_BOOTSTRAP_OPTS=""
-    if [ -n "${OSM_BEHIND_PROXY}" ] ; then
-        K8S_SVC_CLUSTER_IP=$(kubectl get svc/kubernetes -o jsonpath='{.spec.clusterIP}')
-        NO_PROXY="${NO_PROXY},${K8S_SVC_CLUSTER_IP},.svc,.cluster.local"
-        mkdir -p /tmp/.osm
-        JUJU_MODEL_CONFIG_FILE=/tmp/.osm/model-config.yaml
-        cat << EOF > $JUJU_MODEL_CONFIG_FILE
-apt-http-proxy: ${HTTP_PROXY}
-apt-https-proxy: ${HTTPS_PROXY}
-juju-http-proxy: ${HTTP_PROXY}
-juju-https-proxy: ${HTTPS_PROXY}
-juju-no-proxy: ${NO_PROXY}
-snap-http-proxy: ${HTTP_PROXY}
-snap-https-proxy: ${HTTPS_PROXY}
-EOF
-        JUJU_BOOTSTRAP_OPTS="--model-default /tmp/.osm/model-config.yaml"
-    fi
-    juju bootstrap -v --debug $OSM_VCA_K8S_CLOUDNAME $OSM_NAMESPACE  \
-            --config controller-service-type=loadbalancer \
-            --agent-version=$JUJU_AGENT_VERSION \
-            ${JUJU_BOOTSTRAP_OPTS} \
-    || FATAL_TRACK juju "Failed to bootstrap controller $OSM_NAMESPACE in cloud $OSM_VCA_K8S_CLOUDNAME"
-    [ -z "${DEBUG_INSTALL}" ] || DEBUG end of function
-}
-
-function juju_addlxd_cloud(){
-    [ -z "${DEBUG_INSTALL}" ] || DEBUG beginning of function
-    mkdir -p /tmp/.osm
-    OSM_VCA_CLOUDNAME="lxd-cloud"
-    LXDENDPOINT=$DEFAULT_IP
-    LXD_CLOUD=/tmp/.osm/lxd-cloud.yaml
-    LXD_CREDENTIALS=/tmp/.osm/lxd-credentials.yaml
-
-    cat << EOF > $LXD_CLOUD
-clouds:
-  $OSM_VCA_CLOUDNAME:
-    type: lxd
-    auth-types: [certificate]
-    endpoint: "https://$LXDENDPOINT:8443"
-    config:
-      ssl-hostname-verification: false
-EOF
-    openssl req -nodes -new -x509 -keyout /tmp/.osm/client.key -out /tmp/.osm/client.crt -days 365 -subj "/C=FR/ST=Nice/L=Nice/O=ETSI/OU=OSM/CN=osm.etsi.org"
-    cat << EOF > $LXD_CREDENTIALS
-credentials:
-  $OSM_VCA_CLOUDNAME:
-    lxd-cloud:
-      auth-type: certificate
-      server-cert: /var/snap/lxd/common/lxd/server.crt
-      client-cert: /tmp/.osm/client.crt
-      client-key: /tmp/.osm/client.key
-EOF
-    lxc config trust add local: /tmp/.osm/client.crt
-    juju add-cloud -c $OSM_NAMESPACE $OSM_VCA_CLOUDNAME $LXD_CLOUD --force
-    juju add-credential -c $OSM_NAMESPACE $OSM_VCA_CLOUDNAME -f $LXD_CREDENTIALS
-    sg lxd -c "lxd waitready"
-    juju controller-config features=[k8s-operators]
-    if [ -n "${OSM_BEHIND_PROXY}" ] ; then
-        if [ -n "${HTTP_PROXY}" ]; then
-            juju model-default lxd-cloud apt-http-proxy="$HTTP_PROXY"
-            juju model-default lxd-cloud juju-http-proxy="$HTTP_PROXY"
-            juju model-default lxd-cloud snap-http-proxy="$HTTP_PROXY"
-        fi
-        if [ -n "${HTTPS_PROXY}" ]; then
-            juju model-default lxd-cloud apt-https-proxy="$HTTPS_PROXY"
-            juju model-default lxd-cloud juju-https-proxy="$HTTPS_PROXY"
-            juju model-default lxd-cloud snap-https-proxy="$HTTPS_PROXY"
-        fi
-        [ -n "${NO_PROXY}" ] && juju model-default lxd-cloud juju-no-proxy="$NO_PROXY"
-    fi
-    [ -z "${DEBUG_INSTALL}" ] || DEBUG end of function
-}
-
-#Safe unattended install of iptables-persistent
-function check_install_iptables_persistent(){
-    [ -z "${DEBUG_INSTALL}" ] || DEBUG beginning of function
-    echo -e "\nChecking required packages: iptables-persistent"
-    if ! dpkg -l iptables-persistent &>/dev/null; then
-        echo -e "    Not installed.\nInstalling iptables-persistent requires root privileges"
-        echo iptables-persistent iptables-persistent/autosave_v4 boolean true | sudo debconf-set-selections
-        echo iptables-persistent iptables-persistent/autosave_v6 boolean true | sudo debconf-set-selections
-        sudo apt-get -yq install iptables-persistent
-    fi
-    [ -z "${DEBUG_INSTALL}" ] || DEBUG end of function
-}
-
-function juju_createproxy() {
-    [ -z "${DEBUG_INSTALL}" ] || DEBUG beginning of function
-    check_install_iptables_persistent
-
-    if ! sudo iptables -t nat -C PREROUTING -p tcp -m tcp -d $DEFAULT_IP --dport 17070 -j DNAT --to-destination $OSM_VCA_HOST; then
-        sudo iptables -t nat -A PREROUTING -p tcp -m tcp -d $DEFAULT_IP --dport 17070 -j DNAT --to-destination $OSM_VCA_HOST
-        sudo netfilter-persistent save
-    fi
-    [ -z "${DEBUG_INSTALL}" ] || DEBUG end of function
-}
-
-DEBUG_INSTALL=""
-INSTALL_CACHELXDIMAGES=""
-INSTALL_NOJUJU=""
-JUJU_AGENT_VERSION=2.9.43
-JUJU_VERSION=2.9
-OSM_BEHIND_PROXY=""
-OSM_DEVOPS=
-OSM_NAMESPACE=osm
-OSM_VCA_HOST=
-OSM_VCA_CLOUDNAME="localhost"
-OSM_VCA_K8S_CLOUDNAME="k8scloud"
-RE_CHECK='^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'
-
-while getopts ":D:i:s:H:l:L:K:-: hP" o; do
-    case "${o}" in
-        D)
-            OSM_DEVOPS="${OPTARG}"
-            ;;
-        i)
-            DEFAULT_IP="${OPTARG}"
-            ;;
-        s)
-            OSM_NAMESPACE="${OPTARG}" && [[ ! "${OPTARG}" =~ $RE_CHECK ]] && echo "Namespace $OPTARG is invalid. Regex used for validation is $RE_CHECK" && exit 0
-            ;;
-        H)
-            OSM_VCA_HOST="${OPTARG}"
-            ;;
-        l)
-            LXD_CLOUD_FILE="${OPTARG}"
-            ;;
-        L)
-            LXD_CRED_FILE="${OPTARG}"
-            ;;
-        K)
-            CONTROLLER_NAME="${OPTARG}"
-            ;;
-        P)
-            OSM_BEHIND_PROXY="y"
-            ;;
-        -)
-            [ "${OPTARG}" == "help" ] && usage && exit 0
-            [ "${OPTARG}" == "debug" ] && DEBUG_INSTALL="--debug" && continue
-            [ "${OPTARG}" == "cachelxdimages" ] && INSTALL_CACHELXDIMAGES="y" && continue
-            echo -e "Invalid option: '--$OPTARG'\n" >&2
-            usage && exit 1
-            ;;
-        :)
-            echo "Option -$OPTARG requires an argument" >&2
-            usage && exit 1
-            ;;
-        \?)
-            echo -e "Invalid option: '-$OPTARG'\n" >&2
-            usage && exit 1
-            ;;
-        h)
-            usage && exit 0
-            ;;
-        *)
-            usage && exit 1
-            ;;
-    esac
-done
-
-source $OSM_DEVOPS/common/logging
-source $OSM_DEVOPS/common/track
-
-echo "DEBUG_INSTALL=$DEBUG_INSTALL"
-echo "DEFAULT_IP=$DEFAULT_IP"
-echo "OSM_BEHIND_PROXY=$OSM_BEHIND_PROXY"
-echo "OSM_DEVOPS=$OSM_DEVOPS"
-echo "HOME=$HOME"
-
-[ -z "$INSTALL_NOJUJU" ] && install_juju_client
-track juju juju_client_ok
-
-if [ -z "$OSM_VCA_HOST" ]; then
-    if [ -z "$CONTROLLER_NAME" ]; then
-        juju_createcontroller_k8s
-        juju_addlxd_cloud
-        if [ -n "$LXD_CLOUD_FILE" ]; then
-            [ -z "$LXD_CRED_FILE" ] && FATAL_TRACK juju "The installer needs the LXD credential yaml if the LXD is external"
-            OSM_VCA_CLOUDNAME="lxd-cloud"
-            juju add-cloud $OSM_VCA_CLOUDNAME $LXD_CLOUD_FILE --force || juju update-cloud $OSM_VCA_CLOUDNAME --client -f $LXD_CLOUD_FILE
-            juju add-credential $OSM_VCA_CLOUDNAME -f $LXD_CRED_FILE || juju update-credential $OSM_VCA_CLOUDNAME lxd-cloud-creds -f $LXD_CRED_FILE
-        fi
-        juju_createproxy
-    else
-        OSM_VCA_CLOUDNAME="lxd-cloud"
-        if [ -n "$LXD_CLOUD_FILE" ]; then
-            [ -z "$LXD_CRED_FILE" ] && FATAL_TRACK juju "The installer needs the LXD credential yaml if the LXD is external"
-            juju add-cloud -c $CONTROLLER_NAME $OSM_VCA_CLOUDNAME $LXD_CLOUD_FILE --force || juju update-cloud lxd-cloud -c $CONTROLLER_NAME -f $LXD_CLOUD_FILE
-            juju add-credential -c $CONTROLLER_NAME $OSM_VCA_CLOUDNAME -f $LXD_CRED_FILE || juju update-credential lxd-cloud -c $CONTROLLER_NAME -f $LXD_CRED_FILE
-        else
-            mkdir -p ~/.osm
-            cat << EOF > ~/.osm/lxd-cloud.yaml
-clouds:
-  lxd-cloud:
-    type: lxd
-    auth-types: [certificate]
-    endpoint: "https://$DEFAULT_IP:8443"
-    config:
-      ssl-hostname-verification: false
-EOF
-            openssl req -nodes -new -x509 -keyout ~/.osm/client.key -out ~/.osm/client.crt -days 365 -subj "/C=FR/ST=Nice/L=Nice/O=ETSI/OU=OSM/CN=osm.etsi.org"
-            local server_cert=`cat /var/snap/lxd/common/lxd/server.crt | sed 's/^/        /'`
-            local client_cert=`cat ~/.osm/client.crt | sed 's/^/        /'`
-            local client_key=`cat ~/.osm/client.key | sed 's/^/        /'`
-            cat << EOF > ~/.osm/lxd-credentials.yaml
-credentials:
-  lxd-cloud:
-    lxd-cloud:
-      auth-type: certificate
-      server-cert: |
-$server_cert
-      client-cert: |
-$client_cert
-      client-key: |
-$client_key
-EOF
-            lxc config trust add local: ~/.osm/client.crt
-            juju add-cloud -c $CONTROLLER_NAME $OSM_VCA_CLOUDNAME ~/.osm/lxd-cloud.yaml --force || juju update-cloud lxd-cloud -c $CONTROLLER_NAME -f ~/.osm/lxd-cloud.yaml
-            juju add-credential -c $CONTROLLER_NAME $OSM_VCA_CLOUDNAME -f ~/.osm/lxd-credentials.yaml || juju update-credential lxd-cloud -c $CONTROLLER_NAME -f ~/.osm/lxd-credentials.yaml
-        fi
-    fi
-    [ -z "$CONTROLLER_NAME" ] && OSM_VCA_HOST=`sg lxd -c "juju show-controller $OSM_NAMESPACE"|grep api-endpoints|awk -F\' '{print $2}'|awk -F\: '{print $1}'`
-    [ -n "$CONTROLLER_NAME" ] && OSM_VCA_HOST=`juju show-controller $CONTROLLER_NAME |grep api-endpoints|awk -F\' '{print $2}'|awk -F\: '{print $1}'`
-    [ -z "$OSM_VCA_HOST" ] && FATAL_TRACK juju "Cannot obtain juju controller IP address"
-fi
-track juju juju_controller_ok
diff --git a/installers/install_lxd.sh b/installers/install_lxd.sh
deleted file mode 100755 (executable)
index 60cf91e..0000000
+++ /dev/null
@@ -1,130 +0,0 @@
-#!/bin/bash
-#
-#   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.
-#
-
-set +eux
-
-function usage(){
-    [ -z "${DEBUG_INSTALL}" ] || DEBUG beginning of function
-    echo -e "usage: $0 [OPTIONS]"
-    echo -e "Install Juju for OSM"
-    echo -e "  OPTIONS"
-    echo -e "     -h / --help:    print this help"
-    echo -e "     -D <devops path> use local devops installation path"
-    echo -e "     -H <VCA host>   use specific juju host controller IP"
-    echo -e "     -S <VCA secret> use VCA/juju secret key"
-    echo -e "     -P <VCA pubkey> use VCA/juju public key file"
-    echo -e "     -l:             LXD cloud yaml file"
-    echo -e "     -L:             LXD credentials yaml file"
-    echo -e "     -K:             Specifies the name of the controller to use - The controller must be already bootstrapped"
-    echo -e "     --debug:        debug mode"
-    echo -e "     --cachelxdimages:  cache local lxd images, create cronjob for that cache (will make installation longer)"
-    echo -e "     --nojuju:       do not juju, assumes already installed"
-    [ -z "${DEBUG_INSTALL}" ] || DEBUG end of function
-}
-
-function install_lxd() {
-    [ -z "${DEBUG_INSTALL}" ] || DEBUG beginning of function
-    # Apply sysctl production values for optimal performance
-    sudo cp ${OSM_DEVOPS}/installers/lxd/60-lxd-production.conf /etc/sysctl.d/60-lxd-production.conf
-    sudo sysctl --system
-
-    # Install LXD snap
-    sudo apt-get remove --purge -y liblxc1 lxc-common lxcfs lxd lxd-client
-    snap info lxd | grep installed > /dev/null
-    if [ $? -eq 0 ]; then
-        sudo snap refresh lxd --channel $LXD_VERSION/stable
-    else
-        sudo snap install lxd --channel $LXD_VERSION/stable
-    fi
-
-    # Get default iface, IP and MTU
-    if [ -n "${OSM_DEFAULT_IF}" ]; then
-        OSM_DEFAULT_IF=$(ip route list|awk '$1=="default" {print $5; exit}')
-        [ -z "${OSM_DEFAULT_IF}" ] && OSM_DEFAULT_IF=$(route -n |awk '$1~/^0.0.0.0/ {print $8; exit}')
-        [ -z "${OSM_DEFAULT_IF}" ] && FATAL_TRACK lxd "Not possible to determine the interface with the default route 0.0.0.0"
-    fi
-    DEFAULT_MTU=$(ip addr show ${OSM_DEFAULT_IF} | perl -ne 'if (/mtu\s(\d+)/) {print $1;}')
-    OSM_DEFAULT_IP=`ip -o -4 a s ${OSM_DEFAULT_IF} |awk '{split($4,a,"/"); print a[1]; exit}'`
-    [ -z "$OSM_DEFAULT_IP" ] && FATAL_TRACK lxd "Not possible to determine the IP address of the interface with the default route"
-
-    # Configure LXD
-    sudo usermod -a -G lxd `whoami`
-    cat ${OSM_DEVOPS}/installers/lxd/lxd-preseed.conf | sed 's/^config: {}/config:\n  core.https_address: '$OSM_DEFAULT_IP':8443/' | sg lxd -c "lxd init --preseed"
-    sg lxd -c "lxd waitready"
-
-    # Configure LXD to work behind a proxy
-    if [ -n "${OSM_BEHIND_PROXY}" ] ; then
-        [ -n "${HTTP_PROXY}" ] && sg lxd -c "lxc config set core.proxy_http $HTTP_PROXY"
-        [ -n "${HTTPS_PROXY}" ] && sg lxd -c "lxc config set core.proxy_https $HTTPS_PROXY"
-        [ -n "${NO_PROXY}" ] && sg lxd -c "lxc config set core.proxy_ignore_hosts $NO_PROXY"
-    fi
-
-    [ -z "${DEBUG_INSTALL}" ] || DEBUG end of function
-}
-
-DEBUG_INSTALL=""
-LXD_VERSION=5.0
-OSM_DEVOPS=
-OSM_BEHIND_PROXY=""
-
-# main
-while getopts ":D:d:i:-: hP" o; do
-    case "${o}" in
-        i)
-            OSM_DEFAULT_IF="${OPTARG}"
-            ;;
-        d)
-            OSM_DOCKER_WORK_DIR="${OPTARG}"
-            ;;
-        D)
-            OSM_DEVOPS="${OPTARG}"
-            ;;
-        P)
-            OSM_BEHIND_PROXY="y"
-            ;;
-        -)
-            [ "${OPTARG}" == "help" ] && usage && exit 0
-            [ "${OPTARG}" == "debug" ] && DEBUG_INSTALL="y" && continue
-            echo -e "Invalid option: '--$OPTARG'\n" >&2
-            exit 1
-            ;;
-        :)
-            echo "Option -$OPTARG requires an argument" >&2
-            exit 1
-            ;;
-        \?)
-            echo -e "Invalid option: '-$OPTARG'\n" >&2
-            exit 1
-            ;;
-        h)
-            usage && exit 0
-            ;;
-        *)
-            exit 1
-            ;;
-    esac
-done
-
-source $OSM_DEVOPS/common/logging
-source $OSM_DEVOPS/common/track
-
-echo "DEBUG_INSTALL=$DEBUG_INSTALL"
-echo "OSM_BEHIND_PROXY=$OSM_BEHIND_PROXY"
-echo "OSM_DEFAULT_IF=$OSM_DEFAULT_IF"
-echo "OSM_DEVOPS=$OSM_DEVOPS"
-
-[ -z "$INSTALL_NOJUJU" ] && install_lxd
-track prereq lxd_install_ok
-
index 816f39d..d46b7d4 100755 (executable)
@@ -25,7 +25,7 @@ function usage(){
     echo -e "     -h / --help:    print this help"
     echo -e "     -y:             do not prompt for confirmation, assumes yes"
     echo -e "     -r <repo>:      use specified repository name for osm packages"
-    echo -e "     -R <release>:   use specified release for osm binaries (deb packages, lxd images, ...)"
+    echo -e "     -R <release>:   use specified release for osm binaries (deb packages, ...)"
     echo -e "     -u <repo base>: use specified repository url for osm packages"
     echo -e "     -k <repo key>:  use specified repository public key url"
     echo -e "     -a <apt proxy url>: use this apt proxy url when downloading apt packages (air-gapped installation)"
@@ -37,40 +37,17 @@ function usage(){
     echo -e "     --no-aux-cluster: Do not provision an auxiliary cluster for cloud-native gitops operations in OSM (NEW in Release SIXTEEN) (by default, it is installed)"
     echo -e "     -D <devops path>:   use local devops installation path"
     echo -e "     -s <namespace>  namespace when installed using k8s, default is osm"
-    echo -e "     -H <VCA host>   use specific juju host controller IP"
-    echo -e "     -S <VCA secret> use VCA/juju secret key"
-    echo -e "     -P <VCA pubkey> use VCA/juju public key file"
-    echo -e "     -A <VCA apiproxy> use VCA/juju API proxy"
     echo -e "     -w <work dir>:   Location to store runtime installation"
-    echo -e "     -l:             LXD cloud yaml file"
-    echo -e "     -L:             LXD credentials yaml file"
     echo -e "     -K:             Specifies the name of the controller to use - The controller must be already bootstrapped"
     echo -e "     -d <docker registry URL> use docker registry URL instead of dockerhub"
     echo -e "     -p <docker proxy URL> set docker proxy URL as part of docker CE configuration"
     echo -e "     -T <docker tag> specify docker tag for the modules specified with option -m"
     echo -e "     --debug:        debug mode"
-    echo -e "     --nocachelxdimages:  do not cache local lxd images, do not create cronjob for that cache (will save installation time, might affect instantiation time)"
-    echo -e "     --cachelxdimages:  cache local lxd images, create cronjob for that cache (will make installation longer)"
-    echo -e "     --nolxd:        do not install and configure LXD, allowing unattended installations (assumes LXD is already installed and confifured)"
     echo -e "     --nodocker:     do not install docker, do not initialize a swarm (assumes docker is already installed and a swarm has been initialized)"
-    echo -e "     --nojuju:       do not juju, assumes already installed"
-    echo -e "     --nohostports:  do not expose docker ports to host (useful for creating multiple instances of osm on the same host)"
     echo -e "     --nohostclient: do not install the osmclient"
     echo -e "     --uninstall:    uninstall OSM: remove the containers and delete NAT rules"
     echo -e "     --k8s_monitor:  install the OSM kubernetes monitoring with prometheus and grafana"
     echo -e "     --showopts:     print chosen options and exit (only for debugging)"
-    echo -e "     --charmed:                   Deploy and operate OSM with Charms on k8s"
-    echo -e "     [--bundle <bundle path>]:    Specify with which bundle to deploy OSM with charms (--charmed option)"
-    echo -e "     [--k8s <kubeconfig path>]:   Specify with which kubernetes to deploy OSM with charms (--charmed option)"
-    echo -e "     [--vca <name>]:              Specifies the name of the controller to use - The controller must be already bootstrapped (--charmed option)"
-    echo -e "     [--small-profile]:           Do not install and configure LXD which aims to use only K8s Clouds (--charmed option)"
-    echo -e "     [--lxd <yaml path>]:         Takes a YAML file as a parameter with the LXD Cloud information (--charmed option)"
-    echo -e "     [--lxd-cred <yaml path>]:    Takes a YAML file as a parameter with the LXD Credentials information (--charmed option)"
-    echo -e "     [--microstack]:              Installs microstack as a vim. (--charmed option)"
-    echo -e "     [--overlay]:                 Add an overlay to override some defaults of the default bundle (--charmed option)"
-    echo -e "     [--ha]:                      Installs High Availability bundle. (--charmed option)"
-    echo -e "     [--tag]:                     Docker image tag. (--charmed option)"
-    echo -e "     [--registry]:                Docker registry with optional credentials as user:pass@hostname:port (--charmed option)"
 }
 
 add_repo() {
@@ -126,7 +103,7 @@ EOF"
     [ -z "${DEBUG_INSTALL}" ] || DEBUG end of function
 }
 
-while getopts ":a:c:e:r:n:k:u:R:D:o:O:N:H:S:s:t:U:P:A:l:L:K:d:p:T:f:F:G:M:-: hy" o; do
+while getopts ":a:c:e:r:n:k:u:R:D:o:O:N:s:t:U:l:L:K:d:p:T:f:F:G:M:-: hy" o; do
 
     case "${o}" in
         D)
index 1aa9f36..a57c60f 100755 (executable)
@@ -28,12 +28,6 @@ function remove_volumes() {
     [ -z "${DEBUG_INSTALL}" ] || DEBUG end of function
 }
 
-function remove_crontab_job() {
-    [ -z "${DEBUG_INSTALL}" ] || DEBUG beginning of function
-    crontab -l | grep -v '${OSM_DEVOPS}/installers/update-juju-lxc-images'  | crontab -
-    [ -z "${DEBUG_INSTALL}" ] || DEBUG end of function
-}
-
 function uninstall_k8s_monitoring() {
     [ -z "${DEBUG_INSTALL}" ] || DEBUG beginning of function
     # uninstall OSM monitoring
@@ -72,8 +66,6 @@ EONG
 
     [ -z "$CONTROLLER_NAME" ] && sg lxd -c "juju kill-controller -t 0 -y $OSM_NAMESPACE"
 
-    remove_crontab_job
-
     # Cleanup Openstack installer venv
     if [ -d "$OPENSTACK_PYTHON_VENV" ]; then
         rm -r $OPENSTACK_PYTHON_VENV
diff --git a/installers/update-juju-lxc-images b/installers/update-juju-lxc-images
deleted file mode 100755 (executable)
index 18f85c9..0000000
+++ /dev/null
@@ -1,138 +0,0 @@
-#!/bin/bash
-#   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.
-
-#
-# This script will create lxd images that will be used by the
-# lxd provider in juju 2.1+ It is for use with the lxd provider for local
-# development and preinstalls a common set of production packages.
-#
-# This is important, as between them, basenode and layer-basic install ~111
-# packages, before we even get to any packages installed by your charm.
-#
-# It also installs some helpful development tools, and pre-downloads some
-# commonly used packages.
-#
-# This dramatically speeds up the install hooks for lxd deploys. On my slow
-# laptop, average install hook time went from ~7min down to ~1 minute.
-function usage() {
-    echo -e "usage: update-juju-lxc-images [Optional flags]"
-    echo -e "This script will automatically cache all LTS series by default (trusty, xenial, bionic)"
-    echo -e ""
-    echo -e "Optional flags"
-    echo -e "=================="
-    echo -e "--trusty                                   It will download only the trusty series"
-    echo -e "--xenial                                   It will download only the xenial series"
-    echo -e "--bionic                                   It will download only the bionic series"
-    echo -e ""
-    echo -e "Help flags"
-    echo -e "=================="
-    echo -e "-h | --help                                Print full help."
-    exit
-}
-
-FLAGS=0
-trusty=0
-xenial=0
-bionic=0
-while :; do
-    case $1 in
-        --trusty)
-            FLAGS=1
-            trusty=1
-            ;;
-        --xenial)
-            FLAGS=1
-            xenial=1
-            ;;
-        --bionic)
-            FLAGS=1
-            bionic=1
-            ;;
-        -h|--help)
-            usage
-            ;;
-        *)
-             break
-    esac
-    shift
-done
-
-
-set -eux
-
-# The basic charm layer also installs all the things. 47 packages.
-LAYER_BASIC="gcc build-essential python3-pip python3-setuptools python3-yaml"
-
-# the basic layer also installs virtualenv, but the name changed in xenial.
-TRUSTY_PACKAGES="python-virtualenv"
-XENIAL_PACKAGES="virtualenv"
-BIONIC_PACKAGES="virtualenv"
-
-# Predownload common packages used by your charms in development
-DOWNLOAD_PACKAGES=
-
-CLOUD_INIT_PACKAGES="curl cpu-checker bridge-utils cloud-utils tmux ubuntu-fan"
-
-PACKAGES="$LAYER_BASIC $DOWNLOAD_PACKAGES"
-
-JUJU_FULL_VERSION=`juju version` # 2.4.4-bionic-amd64
-JUJU_VERSION=`echo $JUJU_FULL_VERSION | awk -F"-" '{print $1}'`
-OS_VERSION=`echo $JUJU_FULL_VERSION | awk -F"-" '{print $2}'`
-ARCH=`echo $JUJU_FULL_VERSION | awk -F"-" '{print $3}'`
-
-function cache() {
-    series=$1
-    container=juju-${series}-base
-    alias=juju/$series/amd64
-
-    lxc delete $container -f || true
-    lxc image copy ubuntu:$series local: --alias clean-$series
-    lxc launch ubuntu:$series $container
-    sleep 15  # wait for network
-
-    lxc exec $container -- apt-get update -y
-    lxc exec $container -- apt-get upgrade -y
-    lxc exec $container -- apt-get install -y $CLOUD_INIT_PACKAGES $PACKAGES $2
-
-    # Install juju agent
-    echo "Installing Juju agent $JUJU_FULL_VERSION"
-    # TODO: verify if the version exists
-
-    lxc exec $container -- mkdir -p /var/lib/juju/tools/$JUJU_FULL_VERSION/
-
-    lxc exec $container -- curl -sS --connect-timeout 20 --noproxy \* --insecure -o /var/lib/juju/tools/$JUJU_FULL_VERSION/tools.tar.gz  https://streams.canonical.com/juju/tools/agent/$JUJU_VERSION/juju-$JUJU_VERSION-ubuntu-$ARCH.tgz
-
-    lxc exec $container -- tar zxf /var/lib/juju/tools/$JUJU_FULL_VERSION/tools.tar.gz -C /var/lib/juju/tools/$JUJU_FULL_VERSION || true
-
-    # Cache pip packages so installation into venv is faster?
-    # pip3 download --cache-dir ~/.cache/pip charmhelpers
-
-    lxc stop $container
-
-    lxc image delete $alias || true
-    lxc image delete clean-$series || true
-    lxc publish $container --alias $alias description="$series juju dev image ($(date +%Y%m%d))"
-
-    lxc delete $container -f || true
-}
-
-# Enable caching of the serie(s) you're developing for.
-if [ $FLAGS == 0 ]; then
-    cache xenial "$XENIAL_PACKAGES"
-else
-    [ $trusty == 1 ] && cache trusty "$TRUSTY_PACKAGES"
-    [ $xenial == 1 ] && cache xenial "$XENIAL_PACKAGES"
-    [ $bionic == 1 ] && cache bionic "$BIONIC_PACKAGES"
-fi
diff --git a/tools/debug/charmed/README.md b/tools/debug/charmed/README.md
deleted file mode 100644 (file)
index 93bf7ee..0000000
+++ /dev/null
@@ -1,147 +0,0 @@
-<!--
- Copyright 2020 Canonical Ltd.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
-     http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
- For those usages not covered by the Apache License, Version 2.0 please
- contact: legal@canonical.com
-
- To get in touch with the maintainers, please contact:
- osm-charmers@lists.launchpad.net
--->
-
-# Debugging Charmed OSM
-
-This document aims to provide the OSM community an easy way of testing and debugging OSM.
-
-Benefits:
-
-- Use upstream published images for debugging: No need to build local images anymore.
-- Easily configure modules for debugging_mode: `juju config <module> debug_mode=True debug_pubkey="ssh-rsa ..."`.
-- Debug in K8s: All pods (the debugged ones and the rest) will be running always in K8s.
-- Seemless setup: VSCode will connect through SSH to the pods.
-- Keep your changes save: Possibility to mount local module to the container; all the changes will be saved automatically to your local filesystem.
-
-## Install OSM
-
-Download the installer:
-
-```bash
-wget http://osm-download.etsi.org/ftp/osm-10.0-ten/install_osm.sh
-chmod +x install_osm.sh
-```
-
-Install OSM from master (tag=testing-daily):
-
-```bash
-./install_osm.sh -R testing-daily -r testing --charmed
-```
-
-Install OSM from a specific tag:
-
-```bash
-./install_osm.sh -R testing-daily -r testing --charmed --tag <X.Y.Z>
-```
-
-## Debugging
-
-Once the Charmed OSM installation has finished, you can select which applications you want to run with the debug mode.
-
-```bash
-# LCM
-juju config lcm debug_mode=True  debug_pubkey="`cat ~/.ssh/id_rsa.pub`"
-# MON
-juju config mon debug_mode=True  debug_pubkey="`cat ~/.ssh/id_rsa.pub`"
-# NBI
-juju config nbi debug_mode=True  debug_pubkey="`cat ~/.ssh/id_rsa.pub`"
-# RO
-juju config ro debug_mode=True  debug_pubkey="`cat ~/.ssh/id_rsa.pub`"
-# POL
-juju config pol debug_mode=True  debug_pubkey="`cat ~/.ssh/id_rsa.pub`"
-```
-
-Enabling the debug_mode will put a `sleep infinity` as the entrypoint of the container. That way, we can later connect to the pod through SSH in VSCode, and run the entrypoint of the application from the debugger.
-
-### Mounting local modules
-
-The Charmed OSM Debugging mode allows you to mount local modules to the desired charms. The following commands show which modules can be mounted in each charm.
-
-```bash
-LCM_LOCAL_PATH="/path/to/LCM"
-N2VC_LOCAL_PATH="/path/to/N2VC"
-NBI_LOCAL_PATH="/path/to/NBI"
-RO_LOCAL_PATH="/path/to/RO"
-MON_LOCAL_PATH="/path/to/MON"
-POL_LOCAL_PATH="/path/to/POL"
-COMMON_LOCAL_PATH="/path/to/common"
-
-# LCM
-juju config lcm debug_lcm_local_path=$LCM_LOCAL_PATH
-juju config lcm debug_n2vc_local_path=$N2VC_LOCAL_PATH
-juju config lcm debug_common_local_path=$COMMON_LOCAL_PATH
-# MON
-juju config mon debug_mon_local_path=$MON_LOCAL_PATH
-juju config mon debug_n2vc_local_path=$N2VC_LOCAL_PATH
-juju config mon debug_common_local_path=$COMMON_LOCAL_PATH
-# NBI
-juju config nbi debug_nbi_local_path=$LCM_LOCAL_PATH
-juju config nbi debug_common_local_path=$COMMON_LOCAL_PATH
-# RO
-juju config ro debug_ro_local_path=$RO_LOCAL_PATH
-juju config ro debug_common_local_path=$COMMON_LOCAL_PATH
-# POL
-juju config pol debug_pol_local_path=$POL_LOCAL_PATH
-juju config pol debug_common_local_path=$COMMON_LOCAL_PATH
-```
-
-### Generate SSH config file
-
-Preparing the pods includes setting up the `~/.ssh/config` so the VSCode can easily discover which ssh hosts are available
-
-Just execute:
-
-```bash
-./generate_ssh_config.sh
-```
-
-> NOTE: The public key that will be used will be `$HOME/.ssh/id_rsa.pub`. If you want to use a different one, add the absolute path to it as a first argument: `./generate_ssh_config.sh /path/to/key.pub`.
-
-### Connect to Pods
-
-In VScode, navigate to [Remote Explorer](https://code.visualstudio.com/docs/remote/ssh#_remember-hosts-and-advanced-settings), and select the pod to which you want to connect.
-
-You should be able to see the following hosts in the Remote Explorer:
-
-- lcm
-- mon
-- nbi
-- ro
-- pol
-
-Right click on the host, and "Connect to host in a New Window".
-
-### Add workspace
-
-The `./generate_ssh_config.sh` script adds a workspace to the `/root` folder of each pod, with the following name: `debug.code-workspace`.
-
-In the window of the connected host, go to `File/Open Workspace from File...`. Then select the `debug.code-workspace` file.
-
-### Run and Debug
-
-Open the `Terminal` tab, and the Python extension will be automatically downloaded. It will be installed in the remote pod.
-
-Go to the `Explorer (ctrl + shift + E)` to see the module folders in the charm. You can add breakpoints and start debugging. 
-
-Go to the `Run and Debug (ctrl + shift + D)` and press `F5` to start the main entrypoint of the charm.
-
-Happy debugging!
diff --git a/tools/debug/charmed/generate_ssh_config.sh b/tools/debug/charmed/generate_ssh_config.sh
deleted file mode 100755 (executable)
index 58d0686..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-#!/bin/bash
-# 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.
-#
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: legal@canonical.com
-#
-# To get in touch with the maintainers, please contact:
-# osm-charmers@lists.launchpad.net
-##
-
-MODULES="lcm pol mon ro nbi"
-
-
-PRIVATE_KEY=${1:-$HOME/.ssh/id_rsa}
-echo "Using $PRIVATE_KEY key."
-[ -f $PRIVATE_KEY ] || (echo "$PRIVATE_KEY file does not exist" && exit 1)
-PRIVATE_KEY_CONTENT=`cat $PRIVATE_KEY`
-
-mkdir -p ~/.ssh/config.d
-echo "" | tee ~/.ssh/config.d/osm
-
-
-for module in $MODULES; do
-    if [[ `juju config -m osm $module debug_mode` == "true" ]]; then
-      pod_name=`microk8s.kubectl -n osm get pods | grep -E "^$module-" | grep -v operator | cut -d " " -f 1`
-      pod_ip=`microk8s.kubectl -n osm get pods $pod_name -o yaml | yq e .status.podIP -`
-      echo "Host $module
-  HostName $pod_ip
-  User root
-  # StrictHostKeyChecking no
-  IdentityFile $PRIVATE_KEY" | tee -a ~/.ssh/config.d/osm
-    fi
-done
-
-
-import_osm_config="Include config.d/osm"
-touch ~/.ssh/config
-grep "$import_osm_config" ~/.ssh/config || ( echo -e "$import_osm_config\n$(cat ~/.ssh/config)" > ~/.ssh/config )
\ No newline at end of file
diff --git a/tools/promote-charms-and-snaps.sh b/tools/promote-charms-and-snaps.sh
deleted file mode 100755 (executable)
index 1ace0dc..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
-#!/bin/bash
-#
-#   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.
-#
-
-CHANNEL=${1:-latest}
-SOURCE=${2:-beta}
-TARGET=${3:-candidate}
-echo "==========================================================="
-echo Promoting charms and snaps from $SOURCE to $TARGET
-echo ""
-
-for snap in osmclient ; do
-
-    echo "==========================================================="
-    echo "${snap}"
-
-    track="${CHANNEL}/${SOURCE}\\*"
-    SOURCE_REV=$(snapcraft revisions $snap | grep $track | tail -1 | awk '{print $1}')
-    track="${CHANNEL}/${TARGET}\\*"
-    TARGET_REV=$(snapcraft revisions $snap | grep $track | tail -1 | awk '{print $1}')
-
-    echo "$SOURCE: $SOURCE_REV, $TARGET: $TARGET_REV"
-
-    if [ -z $TARGET_REV ] || [ $SOURCE_REV -ne $TARGET_REV ]; then
-        echo "Promoting $SOURCE_REV to beta in place of $TARGET_REV"
-        track="${CHANNEL}/${TARGET}"
-        snapcraft release $snap $SOURCE_REV $track
-    fi
-
-done
-
-for charm in \
-    'osm' \
-    'osm-ha' \
-    'osm-grafana' \
-    'mongodb-exporter-k8s' \
-    'mysqld-exporter-k8s' \
-    'osm-lcm' \
-    'osm-mon' \
-    'osm-nbi' \
-    'osm-ng-ui' \
-    'osm-pol' \
-    'osm-ro' \
-    'osm-prometheus' \
-    'osm-vca-integrator' ; do
-
-    echo "==========================================================="
-    echo "${charm}"
-
-    charmcraft status $charm --format json > ${charm}.json
-    isCharm=$(grep architecture ${charm}.json | wc -l 2>/dev/null)
-    resourceArgument=""
-
-    if [ $isCharm -gt 0 ]; then
-        base=20.04
-        is2204=$(cat ${charm}.json | jq -r ".[] | select(.track==\"$CHANNEL\") | .mappings[] | select(.base.architecture==\"amd64\" and .base.channel==\"22.04\")"|wc -l)
-        if [ $is2204 -gt 0 ]; then
-            base=22.04
-        fi
-
-
-        SOURCE_REV=$(cat ${charm}.json | jq -r ".[] | select(.track==\"$CHANNEL\") | .mappings[] | select(.base.architecture==\"amd64\" and .base.channel==\"$base\") | .releases[] | select(.channel==\"$CHANNEL/$SOURCE\")| .version"|head -1)
-        TARGET_REV=$(cat ${charm}.json | jq -r ".[] | select(.track==\"$CHANNEL\") | .mappings[] | select(.base.architecture==\"amd64\" and .base.channel==\"$base\") | .releases[] | select(.channel==\"$CHANNEL/$TARGET\")| .version"|head -1)
-
-
-        index=0
-        while [ $index -lt 5 ]; do
-            resourceName=$(cat ${charm}.json | jq -r ".[] | select(.track==\"$CHANNEL\") | .mappings[] | select(.base.architecture==\"amd64\" and .base.channel==\"$base\") | .releases[] | select(.channel==\"$CHANNEL/$SOURCE\")| .resources[$index].name"|head -1)
-            resourceRevs=$(cat ${charm}.json | jq -r ".[] | select(.track==\"$CHANNEL\") | .mappings[] | select(.base.architecture==\"amd64\" and .base.channel==\"$base\") | .releases[] | select(.channel==\"$CHANNEL/$SOURCE\")| .resources[$index].revision"|head -1)
-            if [ "$resourceName" != "null" ] ; then
-                resourceArgument=" $resourceArgument --resource ${resourceName}:${resourceRevs}"
-            else
-                break
-            fi
-            ((index=index+1))
-        done
-    else
-        SOURCE_REV=$(cat ${charm}.json | jq -r ".[] | select(.track==\"$CHANNEL\") | .mappings[].releases[] | select(.channel==\"$CHANNEL/$SOURCE\")| .version"|head -1)
-        TARGET_REV=$(cat ${charm}.json | jq -r ".[] | select(.track==\"$CHANNEL\") | .mappings[].releases[] | select(.channel==\"$CHANNEL/$TARGET\")| .version"|head -1)
-    fi
-
-    rm ${charm}.json
-    echo "$SOURCE: $SOURCE_REV, $TARGET: $TARGET_REV $resourceArgument"
-
-    if [ $TARGET_REV == "null" ] || [ $SOURCE_REV -gt $TARGET_REV ] ; then
-        echo Promoting ${charm} revision ${SOURCE_REV} to ${TARGET} ${resourceArgument}
-        charmcraft release ${charm} --revision=${SOURCE_REV}  ${resourceArgument} --channel=${CHANNEL}/$TARGET
-    fi
-
-done