From df70e376ff67a7c248caadc3fd4116b2093fe48b Mon Sep 17 00:00:00 2001 From: Hemanth Nakkina Date: Fri, 17 Nov 2023 10:39:06 +0530 Subject: [PATCH] Add zuuljobs * Add sunbeam project template to run pep8, py3 tests * Add zuul.d/zuul.yaml to run pep8, py3, cover tests * Update charmcraft and requirements for each charm * Add global tox.ini to invoke fmt, pep8, py3, cover, build * Add gitreview file * Fix py3 test failures in ciner-ceph-k8s, glance-k8s, openstack-exporter * Add jobs for charm builds using files option so that job is invoked if files within the component are modified. Add charm builds to both check and gate pipeline. * Make function tests as part of global. Split the function tests into core, ceph, caas, misc mainly to accomodate function tests to run on 8GB. Add function tests as part of check pipeline. * Add zuul job to publish charms in promote pipeline Add charmhub token as secret that can be used to publish charms. Note: Charmhub token is generated with ttl of 90 days. * Run tox formatting * Make .gitignore, .jujuignore, .stestr.conf global and remove the files from all charms. * Make libs and templates global. Split libs to internal and external so that internal libs can adhere to sunbeam formatting styles. * Add script to copy common files necessary libs, config templates, stestr conf, jujuignore during py3 tests and charm builds. * Tests for keystone-ldap-k8s are commented due to intermittent bug LP#2045206 Change-Id: I804ca64182c109d16bd820ac00f129aa6dcf4496 --- charms/aodh-k8s/.gitignore => .gitignore | 6 +- charms/heat-k8s/.gitreview => .gitreview | 2 +- .../keystone-k8s/.jujuignore => .jujuignore | 0 charms/aodh-k8s/.stestr.conf => .stestr.conf | 0 charms/aodh-k8s/.gitreview | 5 - charms/aodh-k8s/.zuul.yaml | 11 - charms/aodh-k8s/charmcraft.yaml | 1 - charms/aodh-k8s/fetch-libs.sh | 7 - .../v0/database_requires.py | 537 ------- .../keystone_k8s/v1/identity_service.py | 525 ------- .../nginx_ingress_integrator/v0/ingress.py | 408 ----- charms/aodh-k8s/osci.yaml | 10 - charms/aodh-k8s/rename.sh | 13 - charms/aodh-k8s/requirements.txt | 4 +- .../src/templates/parts/section-service-user | 15 - charms/aodh-k8s/test-requirements.txt | 11 - charms/aodh-k8s/tests/unit/test_charm.py | 3 +- charms/aodh-k8s/tox.ini | 165 -- charms/barbican-k8s/.flake8 | 9 - charms/barbican-k8s/.gitignore | 11 - charms/barbican-k8s/.gitreview | 5 - charms/barbican-k8s/.jujuignore | 3 - charms/barbican-k8s/.stestr.conf | 3 - charms/barbican-k8s/.zuul.yaml | 11 - charms/barbican-k8s/charmcraft.yaml | 1 - charms/barbican-k8s/fetch-libs.sh | 8 - .../v0/database_requires.py | 537 ------- .../keystone_k8s/v0/identity_resource.py | 392 ----- .../keystone_k8s/v1/identity_service.py | 525 ------- .../lib/charms/rabbitmq_k8s/v0/rabbitmq.py | 286 ---- .../lib/charms/traefik_k8s/v2/ingress.py | 734 --------- charms/barbican-k8s/osci.yaml | 10 - charms/barbican-k8s/pyproject.toml | 39 - charms/barbican-k8s/rename.sh | 13 - charms/barbican-k8s/requirements.txt | 4 +- .../barbican-k8s/src/templates/barbican.conf | 2 + .../src/templates/parts/section-identity | 27 - .../parts/section-oslo-messaging-rabbit | 2 - charms/barbican-k8s/test-requirements.txt | 14 - .../tests/unit/test_barbican_charm.py | 3 +- charms/barbican-k8s/tox.ini | 169 -- charms/ceilometer-k8s/.gitignore | 11 - charms/ceilometer-k8s/.gitreview | 5 - charms/ceilometer-k8s/.stestr.conf | 3 - charms/ceilometer-k8s/.zuul.yaml | 11 - charms/ceilometer-k8s/charmcraft.yaml | 1 - charms/ceilometer-k8s/fetch-libs.sh | 8 - .../lib/charms/rabbitmq_k8s/v0/rabbitmq.py | 286 ---- charms/ceilometer-k8s/osci.yaml | 10 - charms/ceilometer-k8s/pyproject.toml | 39 - charms/ceilometer-k8s/rename.sh | 13 - charms/ceilometer-k8s/requirements.txt | 4 +- .../src/templates/ceilometer.conf | 4 +- .../src/templates/parts/database-connection | 3 - .../src/templates/parts/identity-data | 23 - .../src/templates/parts/section-database | 3 - .../src/templates/parts/section-federation | 10 - .../src/templates/parts/section-identity | 2 - .../src/templates/parts/section-middleware | 6 - .../parts/section-oslo-messaging-rabbit | 2 - .../src/templates/parts/section-service-user | 15 - .../src/templates/parts/section-signing | 15 - charms/ceilometer-k8s/test-requirements.txt | 11 - .../ceilometer-k8s/tests/unit/test_charm.py | 3 +- charms/ceilometer-k8s/tox.ini | 165 -- charms/cinder-ceph-k8s/.flake8 | 9 - charms/cinder-ceph-k8s/.gitignore | 9 - charms/cinder-ceph-k8s/.gitreview | 5 - charms/cinder-ceph-k8s/.jujuignore | 5 - charms/cinder-ceph-k8s/.stestr.conf | 3 - charms/cinder-ceph-k8s/.zuul.yaml | 11 - charms/cinder-ceph-k8s/charmcraft.yaml | 1 - charms/cinder-ceph-k8s/fetch-libs.sh | 8 - .../keystone_k8s/v1/identity_service.py | 518 ------- .../lib/charms/rabbitmq_k8s/v0/rabbitmq.py | 286 ---- .../lib/charms/traefik_k8s/v2/ingress.py | 734 --------- charms/cinder-ceph-k8s/osci.yaml | 12 - charms/cinder-ceph-k8s/pyproject.toml | 39 - charms/cinder-ceph-k8s/rename.sh | 13 - charms/cinder-ceph-k8s/requirements.txt | 4 +- .../parts/section-oslo-messaging-rabbit | 2 - charms/cinder-ceph-k8s/test-requirements.txt | 16 - .../tests/unit/test_cinder_ceph_charm.py | 21 +- charms/cinder-ceph-k8s/tox.ini | 160 -- charms/cinder-k8s/.flake8 | 9 - charms/cinder-k8s/.gitignore | 8 - charms/cinder-k8s/.gitreview | 5 - charms/cinder-k8s/.jujuignore | 3 - charms/cinder-k8s/.stestr.conf | 3 - charms/cinder-k8s/.zuul.yaml | 11 - charms/cinder-k8s/charmcraft.yaml | 1 - charms/cinder-k8s/fetch-libs.sh | 7 - .../charms/cinder_k8s/v0/storage_backend.py | 189 --- .../v0/database_requires.py | 496 ------ .../keystone_k8s/v1/identity_service.py | 525 ------- .../nginx_ingress_integrator/v0/ingress.py | 211 --- .../lib/charms/rabbitmq_k8s/v0/rabbitmq.py | 286 ---- .../lib/charms/traefik_k8s/v2/ingress.py | 734 --------- charms/cinder-k8s/osci.yaml | 10 - charms/cinder-k8s/pyproject.toml | 39 - charms/cinder-k8s/rename.sh | 13 - charms/cinder-k8s/requirements.txt | 4 +- .../src/templates/parts/section-database | 7 - .../src/templates/parts/section-federation | 10 - .../src/templates/parts/section-identity | 24 - .../src/templates/parts/section-middleware | 6 - .../parts/section-oslo-messaging-rabbit | 2 - .../src/templates/parts/section-service-user | 17 - .../src/templates/parts/section-signing | 15 - charms/cinder-k8s/test-requirements.txt | 16 - .../tests/unit/test_cinder_charm.py | 3 +- charms/cinder-k8s/tox.ini | 160 -- charms/designate-bind-k8s/.gitignore | 11 - charms/designate-bind-k8s/.gitreview | 5 - charms/designate-bind-k8s/.stestr.conf | 3 - charms/designate-bind-k8s/.zuul.yaml | 11 - charms/designate-bind-k8s/charmcraft.yaml | 1 - charms/designate-bind-k8s/fetch-libs.sh | 4 - charms/designate-bind-k8s/osci.yaml | 10 - charms/designate-bind-k8s/pyproject.toml | 39 - charms/designate-bind-k8s/rename.sh | 13 - charms/designate-bind-k8s/requirements.txt | 4 +- .../designate-bind-k8s/test-requirements.txt | 9 - .../tests/unit/test_bind_charm.py | 3 +- charms/designate-bind-k8s/tox.ini | 166 -- charms/designate-k8s/.gitignore | 11 - charms/designate-k8s/.gitreview | 5 - charms/designate-k8s/.stestr.conf | 3 - charms/designate-k8s/.zuul.yaml | 11 - charms/designate-k8s/charmcraft.yaml | 1 - charms/designate-k8s/fetch-libs.sh | 9 - .../v0/database_requires.py | 537 ------- .../charms/designate_bind_k8s/v0/bind_rndc.py | 364 ----- .../keystone_k8s/v1/identity_service.py | 525 ------- .../lib/charms/rabbitmq_k8s/v0/rabbitmq.py | 286 ---- .../lib/charms/traefik_k8s/v2/ingress.py | 734 --------- charms/designate-k8s/osci.yaml | 10 - charms/designate-k8s/pyproject.toml | 39 - charms/designate-k8s/rename.sh | 13 - charms/designate-k8s/requirements.txt | 4 +- .../src/templates/parts/database-connection | 3 - .../src/templates/parts/identity-data | 23 - .../src/templates/parts/section-database | 4 - .../src/templates/parts/section-federation | 10 - .../src/templates/parts/section-identity | 2 - .../src/templates/parts/section-middleware | 6 - .../parts/section-oslo-messaging-rabbit | 2 - .../src/templates/parts/section-service-user | 17 - .../src/templates/parts/section-signing | 15 - charms/designate-k8s/test-requirements.txt | 10 - .../tests/unit/test_designate_charm.py | 3 +- charms/designate-k8s/tox.ini | 165 -- charms/glance-k8s/.flake8 | 9 - charms/glance-k8s/.gitignore | 9 - charms/glance-k8s/.gitreview | 5 - charms/glance-k8s/.jujuignore | 3 - charms/glance-k8s/.stestr.conf | 3 - charms/glance-k8s/.zuul.yaml | 11 - charms/glance-k8s/charmcraft.yaml | 1 - charms/glance-k8s/fetch-libs.sh | 7 - .../v0/database_requires.py | 496 ------ .../keystone_k8s/v1/identity_service.py | 525 ------- .../lib/charms/rabbitmq_k8s/v0/rabbitmq.py | 286 ---- .../lib/charms/traefik_k8s/v2/ingress.py | 734 --------- charms/glance-k8s/osci.yaml | 10 - charms/glance-k8s/pyproject.toml | 39 - charms/glance-k8s/rename.sh | 13 - charms/glance-k8s/requirements.txt | 5 +- charms/glance-k8s/src/charm.py | 3 +- .../src/templates/parts/section-database | 7 - .../src/templates/parts/section-federation | 10 - .../src/templates/parts/section-identity | 24 - .../src/templates/parts/section-middleware | 6 - .../parts/section-oslo-messaging-rabbit | 2 - .../parts/section-oslo-notifications | 4 - .../src/templates/parts/section-service-user | 17 - .../src/templates/parts/section-signing | 15 - charms/glance-k8s/test-requirements.txt | 16 - .../tests/unit/test_glance_charm.py | 3 +- charms/glance-k8s/tox.ini | 160 -- charms/gnocchi-k8s/.gitignore | 12 - charms/gnocchi-k8s/.gitreview | 5 - charms/gnocchi-k8s/.stestr.conf | 3 - charms/gnocchi-k8s/.zuul.yaml | 11 - charms/gnocchi-k8s/charmcraft.yaml | 1 - charms/gnocchi-k8s/fetch-libs.sh | 7 - .../v0/database_requires.py | 537 ------- .../charms/gnocchi_k8s/v0/gnocchi_service.py | 205 --- .../keystone_k8s/v1/identity_service.py | 525 ------- .../lib/charms/rabbitmq_k8s/v0/rabbitmq.py | 286 ---- .../lib/charms/traefik_k8s/v2/ingress.py | 734 --------- charms/gnocchi-k8s/osci.yaml | 10 - charms/gnocchi-k8s/pyproject.toml | 39 - charms/gnocchi-k8s/rename.sh | 13 - charms/gnocchi-k8s/requirements.txt | 4 +- .../src/templates/parts/database-connection | 3 - .../src/templates/parts/identity-data | 23 - .../src/templates/parts/section-database | 3 - .../src/templates/parts/section-federation | 10 - .../src/templates/parts/section-identity | 2 - .../src/templates/parts/section-middleware | 6 - .../src/templates/parts/section-signing | 15 - charms/gnocchi-k8s/test-requirements.txt | 11 - charms/gnocchi-k8s/tests/unit/test_charm.py | 3 +- charms/gnocchi-k8s/tox.ini | 165 -- charms/heat-k8s/.gitignore | 11 - charms/heat-k8s/.stestr.conf | 3 - charms/heat-k8s/.zuul.yaml | 11 - charms/heat-k8s/charmcraft.yaml | 1 - charms/heat-k8s/fetch-libs.sh | 8 - .../v0/database_requires.py | 537 ------- .../keystone_k8s/v1/identity_service.py | 525 ------- .../nginx_ingress_integrator/v0/ingress.py | 408 ----- .../lib/charms/rabbitmq_k8s/v0/rabbitmq.py | 286 ---- .../lib/charms/traefik_k8s/v2/ingress.py | 734 --------- charms/heat-k8s/osci.yaml | 10 - charms/heat-k8s/pyproject.toml | 39 - charms/heat-k8s/rename.sh | 13 - charms/heat-k8s/requirements.txt | 4 +- .../src/templates/parts/section-database | 7 - .../src/templates/parts/section-federation | 10 - .../src/templates/parts/section-identity | 38 - .../src/templates/parts/section-middleware | 6 - .../parts/section-oslo-messaging-rabbit | 2 - .../src/templates/parts/section-signing | 15 - charms/heat-k8s/test-requirements.txt | 17 - charms/heat-k8s/tests/unit/test_heat_charm.py | 3 +- charms/heat-k8s/tox.ini | 170 --- charms/horizon-k8s/.flake8 | 9 - charms/horizon-k8s/.gitignore | 8 - charms/horizon-k8s/.gitreview | 5 - charms/horizon-k8s/.jujuignore | 3 - charms/horizon-k8s/.stestr.conf | 3 - charms/horizon-k8s/.zuul.yaml | 4 - charms/horizon-k8s/charmcraft.yaml | 1 - charms/horizon-k8s/fetch-libs.sh | 9 - .../v0/database_requires.py | 496 ------ .../keystone_k8s/v0/identity_credentials.py | 439 ------ .../keystone_k8s/v1/cloud_credentials.py | 439 ------ .../keystone_k8s/v1/identity_service.py | 518 ------- .../nginx_ingress_integrator/v0/ingress.py | 227 --- .../lib/charms/rabbitmq_k8s/v0/rabbitmq.py | 286 ---- .../lib/charms/traefik_k8s/v2/ingress.py | 734 --------- charms/horizon-k8s/osci.yaml | 12 - charms/horizon-k8s/pyproject.toml | 39 - charms/horizon-k8s/rename.sh | 13 - charms/horizon-k8s/requirements.txt | 4 +- .../src/templates/parts/database-connection | 3 - charms/horizon-k8s/test-requirements.txt | 12 - .../tests/unit/test_horizon_charm.py | 3 +- charms/horizon-k8s/tox.ini | 132 -- charms/keystone-k8s/.flake8 | 9 - charms/keystone-k8s/.gitignore | 11 - charms/keystone-k8s/.gitreview | 5 - charms/keystone-k8s/.stestr.conf | 3 - charms/keystone-k8s/.zuul.yaml | 11 - charms/keystone-k8s/charmcraft.yaml | 1 - charms/keystone-k8s/fetch-libs.sh | 9 - .../v0/database_requires.py | 496 ------ .../keystone_k8s/v0/identity_credentials.py | 458 ------ .../keystone_k8s/v0/identity_resource.py | 393 ----- .../nginx_ingress_integrator/v0/ingress.py | 227 --- .../lib/charms/rabbitmq_k8s/v0/rabbitmq.py | 286 ---- .../lib/charms/traefik_k8s/v2/ingress.py | 734 --------- charms/keystone-k8s/osci.yaml | 10 - charms/keystone-k8s/pyproject.toml | 39 - charms/keystone-k8s/rename.sh | 13 - charms/keystone-k8s/requirements.txt | 5 +- charms/keystone-k8s/src/charm.py | 1 - .../src/templates/parts/section-database | 7 - .../src/templates/parts/section-federation | 10 - .../src/templates/parts/section-middleware | 6 - .../parts/section-oslo-messaging-rabbit | 2 - .../parts/section-oslo-notifications | 4 - .../src/templates/parts/section-signing | 15 - charms/keystone-k8s/src/utils/manager.py | 1 - charms/keystone-k8s/test-requirements.txt | 17 - .../tests/unit/test_keystone_charm.py | 3 +- charms/keystone-k8s/tox.ini | 161 -- charms/keystone-ldap-k8s/.gitignore | 11 - charms/keystone-ldap-k8s/.gitreview | 5 - charms/keystone-ldap-k8s/.stestr.conf | 3 - charms/keystone-ldap-k8s/.zuul.yaml | 11 - charms/keystone-ldap-k8s/charmcraft.yaml | 1 - .../charms/keystone_k8s/v0/domain_config.py | 159 -- charms/keystone-ldap-k8s/osci.yaml | 10 - charms/keystone-ldap-k8s/pyproject.toml | 33 - charms/keystone-ldap-k8s/rename.sh | 13 - charms/keystone-ldap-k8s/requirements.txt | 5 +- charms/keystone-ldap-k8s/src/charm.py | 14 +- .../keystone-ldap-k8s/test-requirements.txt | 17 - .../tests/integration/test_charm.py | 41 +- .../tests/unit/test_keystone_ldap_charm.py | 13 +- charms/keystone-ldap-k8s/tox.ini | 161 -- charms/magnum-k8s/.flake8 | 9 - charms/magnum-k8s/.gitignore | 10 - charms/magnum-k8s/.gitreview | 5 - charms/magnum-k8s/.jujuignore | 3 - charms/magnum-k8s/.stestr.conf | 3 - charms/magnum-k8s/.zuul.yaml | 11 - charms/magnum-k8s/charmcraft.yaml | 1 - charms/magnum-k8s/fetch-libs.sh | 8 - .../v0/database_requires.py | 537 ------- .../keystone_k8s/v0/identity_resource.py | 373 ----- .../keystone_k8s/v1/identity_service.py | 525 ------- .../lib/charms/rabbitmq_k8s/v0/rabbitmq.py | 286 ---- .../lib/charms/traefik_k8s/v2/ingress.py | 734 --------- charms/magnum-k8s/osci.yaml | 10 - charms/magnum-k8s/pyproject.toml | 40 - charms/magnum-k8s/rename.sh | 13 - charms/magnum-k8s/requirements.txt | 4 +- .../magnum-k8s/src/templates/magnum.conf.j2 | 7 +- .../src/templates/parts/database-connection | 3 - .../src/templates/parts/identity-data | 26 - .../src/templates/parts/section-database | 3 - .../src/templates/parts/section-identity | 5 - .../parts/section-oslo-messaging-rabbit | 2 - .../src/templates/parts/section-service-user | 17 - charms/magnum-k8s/test-requirements.txt | 17 - .../tests/unit/test_magnum_charm.py | 3 +- charms/magnum-k8s/tox.ini | 161 -- charms/neutron-k8s/.flake8 | 9 - charms/neutron-k8s/.gitignore | 11 - charms/neutron-k8s/.gitreview | 5 - charms/neutron-k8s/.jujuignore | 3 - charms/neutron-k8s/.stestr.conf | 3 - charms/neutron-k8s/.zuul.yaml | 11 - charms/neutron-k8s/charmcraft.yaml | 1 - charms/neutron-k8s/fetch-libs.sh | 9 - .../v0/database_requires.py | 496 ------ .../keystone_k8s/v1/identity_service.py | 525 ------- .../lib/charms/rabbitmq_k8s/v0/rabbitmq.py | 286 ---- .../lib/charms/traefik_k8s/v2/ingress.py | 734 --------- charms/neutron-k8s/osci.yaml | 10 - charms/neutron-k8s/pyproject.toml | 39 - charms/neutron-k8s/rename.sh | 13 - charms/neutron-k8s/requirements.txt | 3 +- .../src/templates/parts/section-database | 7 - .../src/templates/parts/section-federation | 10 - .../src/templates/parts/section-identity | 24 - .../src/templates/parts/section-middleware | 6 - .../parts/section-oslo-messaging-rabbit | 2 - .../src/templates/parts/section-service-user | 17 - .../src/templates/parts/section-signing | 15 - charms/neutron-k8s/test-requirements.txt | 13 - .../tests/unit/test_neutron_charm.py | 3 +- charms/neutron-k8s/tox.ini | 169 -- charms/nova-k8s/.flake8 | 9 - charms/nova-k8s/.gitignore | 11 - charms/nova-k8s/.gitreview | 5 - charms/nova-k8s/.jujuignore | 3 - charms/nova-k8s/.stestr.conf | 3 - charms/nova-k8s/.zuul.yaml | 11 - charms/nova-k8s/charmcraft.yaml | 1 - charms/nova-k8s/fetch-libs.sh | 8 - .../v0/database_requires.py | 496 ------ .../keystone_k8s/v1/identity_service.py | 525 ------- .../lib/charms/rabbitmq_k8s/v0/rabbitmq.py | 286 ---- .../lib/charms/traefik_k8s/v2/ingress.py | 734 --------- charms/nova-k8s/osci.yaml | 10 - charms/nova-k8s/pyproject.toml | 39 - charms/nova-k8s/rename.sh | 13 - charms/nova-k8s/requirements.txt | 4 +- charms/nova-k8s/src/templates/nova.conf.j2 | 15 +- .../src/templates/parts/section-identity | 25 - .../parts/section-oslo-messaging-rabbit | 2 - .../src/templates/parts/section-service-user | 17 - charms/nova-k8s/test-requirements.txt | 16 - charms/nova-k8s/tests/unit/test_nova_charm.py | 3 +- charms/nova-k8s/tox.ini | 161 -- charms/octavia-k8s/.gitignore | 11 - charms/octavia-k8s/.gitreview | 5 - charms/octavia-k8s/.stestr.conf | 3 - charms/octavia-k8s/.zuul.yaml | 11 - charms/octavia-k8s/charmcraft.yaml | 1 - charms/octavia-k8s/fetch-libs.sh | 9 - .../v0/database_requires.py | 537 ------- .../keystone_k8s/v0/identity_resource.py | 392 ----- .../keystone_k8s/v1/identity_service.py | 525 ------- .../lib/charms/ovn_central_k8s/v0/ovsdb.py | 206 --- .../v1/tls_certificates.py | 1360 ----------------- .../lib/charms/traefik_k8s/v2/ingress.py | 734 --------- charms/octavia-k8s/osci.yaml | 10 - charms/octavia-k8s/pyproject.toml | 39 - charms/octavia-k8s/rename.sh | 13 - charms/octavia-k8s/requirements.txt | 4 +- .../src/templates/parts/database-connection | 3 - .../src/templates/parts/identity-data | 21 - .../src/templates/parts/section-database | 3 - .../src/templates/parts/section-federation | 10 - .../src/templates/parts/section-identity | 3 - .../src/templates/parts/section-middleware | 6 - .../src/templates/parts/section-signing | 15 - .../src/templates/parts/service-token | 2 - charms/octavia-k8s/test-requirements.txt | 9 - charms/octavia-k8s/tests/unit/test_charm.py | 3 +- charms/octavia-k8s/tox.ini | 165 -- charms/openstack-exporter-k8s/.gitignore | 11 - charms/openstack-exporter-k8s/.gitreview | 5 - charms/openstack-exporter-k8s/.stestr.conf | 3 - charms/openstack-exporter-k8s/.zuul.yaml | 11 - charms/openstack-exporter-k8s/charmcraft.yaml | 1 - .../keystone_k8s/v0/identity_resource.py | 393 ----- .../v1/tls_certificates.py | 1360 ----------------- charms/openstack-exporter-k8s/osci.yaml | 10 - charms/openstack-exporter-k8s/pyproject.toml | 39 - charms/openstack-exporter-k8s/rename.sh | 13 - .../openstack-exporter-k8s/requirements.txt | 6 +- .../test-requirements.txt | 9 - .../tests/unit/test_os_exporter.py | 3 +- charms/openstack-exporter-k8s/tox.ini | 161 -- charms/openstack-hypervisor/.gitignore | 10 - charms/openstack-hypervisor/.gitreview | 5 - charms/openstack-hypervisor/.stestr.conf | 3 - charms/openstack-hypervisor/.zuul.yaml | 12 - charms/openstack-hypervisor/charmcraft.yaml | 1 - .../ceilometer_k8s/v0/ceilometer_service.py | 224 --- .../charms/cinder_ceph_k8s/v0/ceph_access.py | 266 ---- .../v0/database_requires.py | 496 ------ .../keystone_k8s/v0/cloud_credentials.py | 418 ----- .../keystone_k8s/v0/identity_credentials.py | 458 ------ .../keystone_k8s/v0/identity_service.py | 493 ------ .../keystone_k8s/v1/cloud_credentials.py | 451 ------ .../keystone_k8s/v1/identity_service.py | 518 ------- .../v0/kubernetes_service_patch.py | 280 ---- .../v1/kubernetes_service_patch.py | 342 ----- .../lib/charms/ovn_central_k8s/v0/ovsdb.py | 218 --- .../lib/charms/rabbitmq_k8s/v0/rabbitmq.py | 286 ---- .../v1/tls_certificates.py | 1261 --------------- .../lib/charms/traefik_k8s/v1/ingress.py | 558 ------- charms/openstack-hypervisor/osci.yaml | 10 - charms/openstack-hypervisor/pyproject.toml | 39 - charms/openstack-hypervisor/rename.sh | 13 - charms/openstack-hypervisor/requirements.txt | 5 +- charms/openstack-hypervisor/src/charm.py | 1 - .../test-requirements.txt | 15 - .../tests/unit/test_charm.py | 3 +- charms/openstack-hypervisor/tox.ini | 168 -- charms/ovn-central-k8s/.flake8 | 9 - charms/ovn-central-k8s/.gitignore | 11 - charms/ovn-central-k8s/.gitreview | 5 - charms/ovn-central-k8s/.jujuignore | 3 - charms/ovn-central-k8s/.stestr.conf | 3 - charms/ovn-central-k8s/.zuul.yaml | 11 - charms/ovn-central-k8s/charmcraft.yaml | 1 - charms/ovn-central-k8s/fetch-libs.sh | 4 - .../lib/charms/ovn_central_k8s/v0/ovsdb.py | 206 --- .../v1/tls_certificates.py | 1261 --------------- charms/ovn-central-k8s/osci.yaml | 10 - charms/ovn-central-k8s/pyproject.toml | 39 - charms/ovn-central-k8s/rename.sh | 13 - charms/ovn-central-k8s/requirements.txt | 3 +- charms/ovn-central-k8s/src/charm.py | 5 +- charms/ovn-central-k8s/test-requirements.txt | 12 - .../tests/unit/test_ovn_central_charm.py | 3 +- charms/ovn-central-k8s/tox.ini | 169 -- charms/ovn-relay-k8s/.flake8 | 9 - charms/ovn-relay-k8s/.gitignore | 10 - charms/ovn-relay-k8s/.gitreview | 5 - charms/ovn-relay-k8s/.jujuignore | 3 - charms/ovn-relay-k8s/.stestr.conf | 3 - charms/ovn-relay-k8s/.zuul.yaml | 11 - charms/ovn-relay-k8s/charmcraft.yaml | 1 - charms/ovn-relay-k8s/fetch-libs.sh | 6 - .../v1/kubernetes_service_patch.py | 342 ----- .../lib/charms/ovn_central_k8s/v0/ovsdb.py | 206 --- .../v1/tls_certificates.py | 1261 --------------- charms/ovn-relay-k8s/osci.yaml | 10 - charms/ovn-relay-k8s/pyproject.toml | 39 - charms/ovn-relay-k8s/rename.sh | 13 - charms/ovn-relay-k8s/requirements.txt | 3 +- charms/ovn-relay-k8s/test-requirements.txt | 12 - .../tests/unit/test_ovn_relay_charm.py | 3 +- charms/ovn-relay-k8s/tox.ini | 169 -- charms/placement-k8s/.flake8 | 9 - charms/placement-k8s/.gitignore | 11 - charms/placement-k8s/.gitreview | 5 - charms/placement-k8s/.jujuignore | 3 - charms/placement-k8s/.stestr.conf | 3 - charms/placement-k8s/.zuul.yaml | 11 - charms/placement-k8s/charmcraft.yaml | 1 - charms/placement-k8s/fetch-libs.sh | 7 - .../v0/database_requires.py | 496 ------ .../keystone_k8s/v1/identity_service.py | 525 ------- .../lib/charms/rabbitmq_k8s/v0/rabbitmq.py | 286 ---- .../lib/charms/traefik_k8s/v2/ingress.py | 734 --------- charms/placement-k8s/osci.yaml | 10 - charms/placement-k8s/pyproject.toml | 39 - charms/placement-k8s/rename.sh | 13 - charms/placement-k8s/requirements.txt | 3 +- .../src/templates/parts/section-database | 7 - .../src/templates/parts/section-federation | 10 - .../src/templates/parts/section-identity | 24 - .../src/templates/parts/section-middleware | 6 - .../src/templates/parts/section-service-user | 17 - .../src/templates/parts/section-signing | 15 - .../src/templates/placement.conf | 6 +- charms/placement-k8s/test-requirements.txt | 16 - .../tests/unit/test_placement_charm.py | 5 +- charms/placement-k8s/tox.ini | 161 -- common.sh | 418 +++++ fetch_libs.sh | 17 + .../v0/database_requires.py | 0 .../lib/charms/grafana_agent/v0/cos_agent.py | 0 .../grafana_k8s/v0/grafana_dashboard.py | 0 .../v1/kubernetes_service_patch.py | 0 .../lib/charms/operator_libs_linux/v2/snap.py | 0 .../prometheus_k8s/v0/prometheus_scrape.py | 0 .../lib/charms/rabbitmq_k8s/v0/rabbitmq.py | 0 .../v1/tls_certificates.py | 0 .../lib/charms/traefik_k8s/v2/ingress.py | 0 .../traefik_route_k8s/v0/traefik_route.py | 0 .../lib/charms/vault_k8s/v0/vault_kv.py | 0 .../ceilometer_k8s/v0/ceilometer_service.py | 0 .../charms/cinder_ceph_k8s/v0/ceph_access.py | 0 .../charms/cinder_k8s/v0/storage_backend.py | 0 .../charms/designate_bind_k8s/v0/bind_rndc.py | 0 .../charms/gnocchi_k8s/v0/gnocchi_service.py | 0 .../charms/gnocchi_k8s/v0/metric_service.py | 0 .../keystone_k8s/v0/cloud_credentials.py | 0 .../charms/keystone_k8s/v0/domain_config.py | 0 .../keystone_k8s/v0/identity_credentials.py | 0 .../keystone_k8s/v0/identity_resource.py | 0 .../keystone_k8s/v0/identity_service.py | 0 .../keystone_k8s/v1/cloud_credentials.py | 0 .../keystone_k8s/v1/identity_service.py | 0 .../lib/charms/ovn_central_k8s/v0/ovsdb.py | 0 .../v0/cloud_compute.py | 0 ops-sunbeam/pyproject.toml | 39 - ops-sunbeam/test-requirements.txt | 6 - ops-sunbeam/tox.ini | 135 -- playbooks/charm/build.yaml | 6 + playbooks/charm/publish.yaml | 3 + playbooks/collect-run-data.yaml | 3 + playbooks/zaza-func-test.yaml | 6 + .../aodh-k8s/pyproject.toml => pyproject.toml | 0 render_bundles.py | 54 + roles/charm-build/tasks/main.yaml | 69 + roles/charm-publish/defaults/main.yaml | 23 + roles/charm-publish/tasks/main.yaml | 53 + roles/charm-publish/tasks/publish.yaml | 59 + roles/collect-run-data/tasks/main.yaml | 95 ++ roles/microk8s-cloud/tasks/main.yaml | 157 ++ roles/microk8s-cloud/templates/hosts.j2 | 4 + roles/zaza-func-test/tasks/main.yaml | 31 + run_tox.sh | 110 ++ .../parts/database-connection | 2 + .../parts/identity-data | 0 .../parts/identity-data-id-creds | 0 .../parts/section-certificates | 0 .../parts/section-database | 0 .../parts/section-federation | 0 .../parts/section-identity | 0 .../parts/section-middleware | 0 .../parts/section-oslo-cache | 0 .../parts/section-oslo-messaging-rabbit | 0 .../parts/section-oslo-middleware | 0 .../parts/section-oslo-notifications | 0 .../parts/section-service-credentials | 0 ...-service-credentials-from-identity-service | 0 .../parts/section-service-user | 0 ...ion-service-user-from-identity-credentials | 0 .../parts/section-signing | 0 .../parts/section-trust | 0 test-requirements.txt | 17 + tests/caas/smoke.yaml.j2 | 190 +++ tests/caas/tests.yaml | 56 + tests/ceph/smoke.yaml.j2 | 145 ++ tests/ceph/tests.yaml | 43 + tests/core/smoke.yaml.j2 | 192 +++ tests/core/tests.yaml | 72 + tests/misc/smoke.yaml.j2 | 105 ++ tests/misc/tests.yaml | 52 + tox.ini | 93 ++ zuul.d/jobs.yaml | 410 +++++ zuul.d/project-templates.yaml | 137 ++ zuul.d/secrets.yaml | 65 + zuul.d/zuul.yaml | 48 + 578 files changed, 2917 insertions(+), 54303 deletions(-) rename charms/aodh-k8s/.gitignore => .gitignore (84%) rename charms/heat-k8s/.gitreview => .gitreview (63%) rename charms/keystone-k8s/.jujuignore => .jujuignore (100%) rename charms/aodh-k8s/.stestr.conf => .stestr.conf (100%) delete mode 100644 charms/aodh-k8s/.gitreview delete mode 100644 charms/aodh-k8s/.zuul.yaml delete mode 100755 charms/aodh-k8s/fetch-libs.sh delete mode 100644 charms/aodh-k8s/lib/charms/data_platform_libs/v0/database_requires.py delete mode 100644 charms/aodh-k8s/lib/charms/keystone_k8s/v1/identity_service.py delete mode 100644 charms/aodh-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py delete mode 100644 charms/aodh-k8s/osci.yaml delete mode 100755 charms/aodh-k8s/rename.sh delete mode 100644 charms/aodh-k8s/src/templates/parts/section-service-user delete mode 100644 charms/aodh-k8s/test-requirements.txt delete mode 100644 charms/aodh-k8s/tox.ini delete mode 100644 charms/barbican-k8s/.flake8 delete mode 100644 charms/barbican-k8s/.gitignore delete mode 100644 charms/barbican-k8s/.gitreview delete mode 100644 charms/barbican-k8s/.jujuignore delete mode 100644 charms/barbican-k8s/.stestr.conf delete mode 100644 charms/barbican-k8s/.zuul.yaml delete mode 100755 charms/barbican-k8s/fetch-libs.sh delete mode 100644 charms/barbican-k8s/lib/charms/data_platform_libs/v0/database_requires.py delete mode 100644 charms/barbican-k8s/lib/charms/keystone_k8s/v0/identity_resource.py delete mode 100644 charms/barbican-k8s/lib/charms/keystone_k8s/v1/identity_service.py delete mode 100644 charms/barbican-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py delete mode 100644 charms/barbican-k8s/lib/charms/traefik_k8s/v2/ingress.py delete mode 100644 charms/barbican-k8s/osci.yaml delete mode 100644 charms/barbican-k8s/pyproject.toml delete mode 100755 charms/barbican-k8s/rename.sh delete mode 100644 charms/barbican-k8s/src/templates/parts/section-identity delete mode 100644 charms/barbican-k8s/src/templates/parts/section-oslo-messaging-rabbit delete mode 100644 charms/barbican-k8s/test-requirements.txt delete mode 100644 charms/barbican-k8s/tox.ini delete mode 100644 charms/ceilometer-k8s/.gitignore delete mode 100644 charms/ceilometer-k8s/.gitreview delete mode 100644 charms/ceilometer-k8s/.stestr.conf delete mode 100644 charms/ceilometer-k8s/.zuul.yaml delete mode 100755 charms/ceilometer-k8s/fetch-libs.sh delete mode 100644 charms/ceilometer-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py delete mode 100644 charms/ceilometer-k8s/osci.yaml delete mode 100644 charms/ceilometer-k8s/pyproject.toml delete mode 100755 charms/ceilometer-k8s/rename.sh delete mode 100644 charms/ceilometer-k8s/src/templates/parts/database-connection delete mode 100644 charms/ceilometer-k8s/src/templates/parts/identity-data delete mode 100644 charms/ceilometer-k8s/src/templates/parts/section-database delete mode 100644 charms/ceilometer-k8s/src/templates/parts/section-federation delete mode 100644 charms/ceilometer-k8s/src/templates/parts/section-identity delete mode 100644 charms/ceilometer-k8s/src/templates/parts/section-middleware delete mode 100644 charms/ceilometer-k8s/src/templates/parts/section-oslo-messaging-rabbit delete mode 100644 charms/ceilometer-k8s/src/templates/parts/section-service-user delete mode 100644 charms/ceilometer-k8s/src/templates/parts/section-signing delete mode 100644 charms/ceilometer-k8s/test-requirements.txt delete mode 100644 charms/ceilometer-k8s/tox.ini delete mode 100644 charms/cinder-ceph-k8s/.flake8 delete mode 100644 charms/cinder-ceph-k8s/.gitignore delete mode 100644 charms/cinder-ceph-k8s/.gitreview delete mode 100644 charms/cinder-ceph-k8s/.jujuignore delete mode 100644 charms/cinder-ceph-k8s/.stestr.conf delete mode 100644 charms/cinder-ceph-k8s/.zuul.yaml delete mode 100755 charms/cinder-ceph-k8s/fetch-libs.sh delete mode 100644 charms/cinder-ceph-k8s/lib/charms/keystone_k8s/v1/identity_service.py delete mode 100644 charms/cinder-ceph-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py delete mode 100644 charms/cinder-ceph-k8s/lib/charms/traefik_k8s/v2/ingress.py delete mode 100644 charms/cinder-ceph-k8s/osci.yaml delete mode 100644 charms/cinder-ceph-k8s/pyproject.toml delete mode 100755 charms/cinder-ceph-k8s/rename.sh delete mode 100644 charms/cinder-ceph-k8s/src/templates/parts/section-oslo-messaging-rabbit delete mode 100644 charms/cinder-ceph-k8s/test-requirements.txt delete mode 100644 charms/cinder-ceph-k8s/tox.ini delete mode 100644 charms/cinder-k8s/.flake8 delete mode 100644 charms/cinder-k8s/.gitignore delete mode 100644 charms/cinder-k8s/.gitreview delete mode 100644 charms/cinder-k8s/.jujuignore delete mode 100644 charms/cinder-k8s/.stestr.conf delete mode 100644 charms/cinder-k8s/.zuul.yaml delete mode 100755 charms/cinder-k8s/fetch-libs.sh delete mode 100644 charms/cinder-k8s/lib/charms/cinder_k8s/v0/storage_backend.py delete mode 100644 charms/cinder-k8s/lib/charms/data_platform_libs/v0/database_requires.py delete mode 100644 charms/cinder-k8s/lib/charms/keystone_k8s/v1/identity_service.py delete mode 100644 charms/cinder-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py delete mode 100644 charms/cinder-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py delete mode 100644 charms/cinder-k8s/lib/charms/traefik_k8s/v2/ingress.py delete mode 100644 charms/cinder-k8s/osci.yaml delete mode 100644 charms/cinder-k8s/pyproject.toml delete mode 100755 charms/cinder-k8s/rename.sh delete mode 100644 charms/cinder-k8s/src/templates/parts/section-database delete mode 100644 charms/cinder-k8s/src/templates/parts/section-federation delete mode 100644 charms/cinder-k8s/src/templates/parts/section-identity delete mode 100644 charms/cinder-k8s/src/templates/parts/section-middleware delete mode 100644 charms/cinder-k8s/src/templates/parts/section-oslo-messaging-rabbit delete mode 100644 charms/cinder-k8s/src/templates/parts/section-service-user delete mode 100644 charms/cinder-k8s/src/templates/parts/section-signing delete mode 100644 charms/cinder-k8s/test-requirements.txt delete mode 100644 charms/cinder-k8s/tox.ini delete mode 100644 charms/designate-bind-k8s/.gitignore delete mode 100644 charms/designate-bind-k8s/.gitreview delete mode 100644 charms/designate-bind-k8s/.stestr.conf delete mode 100644 charms/designate-bind-k8s/.zuul.yaml delete mode 100755 charms/designate-bind-k8s/fetch-libs.sh delete mode 100644 charms/designate-bind-k8s/osci.yaml delete mode 100644 charms/designate-bind-k8s/pyproject.toml delete mode 100755 charms/designate-bind-k8s/rename.sh delete mode 100644 charms/designate-bind-k8s/test-requirements.txt delete mode 100644 charms/designate-bind-k8s/tox.ini delete mode 100644 charms/designate-k8s/.gitignore delete mode 100644 charms/designate-k8s/.gitreview delete mode 100644 charms/designate-k8s/.stestr.conf delete mode 100644 charms/designate-k8s/.zuul.yaml delete mode 100755 charms/designate-k8s/fetch-libs.sh delete mode 100644 charms/designate-k8s/lib/charms/data_platform_libs/v0/database_requires.py delete mode 100644 charms/designate-k8s/lib/charms/designate_bind_k8s/v0/bind_rndc.py delete mode 100644 charms/designate-k8s/lib/charms/keystone_k8s/v1/identity_service.py delete mode 100644 charms/designate-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py delete mode 100644 charms/designate-k8s/lib/charms/traefik_k8s/v2/ingress.py delete mode 100644 charms/designate-k8s/osci.yaml delete mode 100644 charms/designate-k8s/pyproject.toml delete mode 100755 charms/designate-k8s/rename.sh delete mode 100644 charms/designate-k8s/src/templates/parts/database-connection delete mode 100644 charms/designate-k8s/src/templates/parts/identity-data delete mode 100644 charms/designate-k8s/src/templates/parts/section-database delete mode 100644 charms/designate-k8s/src/templates/parts/section-federation delete mode 100644 charms/designate-k8s/src/templates/parts/section-identity delete mode 100644 charms/designate-k8s/src/templates/parts/section-middleware delete mode 100644 charms/designate-k8s/src/templates/parts/section-oslo-messaging-rabbit delete mode 100644 charms/designate-k8s/src/templates/parts/section-service-user delete mode 100644 charms/designate-k8s/src/templates/parts/section-signing delete mode 100644 charms/designate-k8s/test-requirements.txt delete mode 100644 charms/designate-k8s/tox.ini delete mode 100644 charms/glance-k8s/.flake8 delete mode 100644 charms/glance-k8s/.gitignore delete mode 100644 charms/glance-k8s/.gitreview delete mode 100644 charms/glance-k8s/.jujuignore delete mode 100644 charms/glance-k8s/.stestr.conf delete mode 100644 charms/glance-k8s/.zuul.yaml delete mode 100755 charms/glance-k8s/fetch-libs.sh delete mode 100644 charms/glance-k8s/lib/charms/data_platform_libs/v0/database_requires.py delete mode 100644 charms/glance-k8s/lib/charms/keystone_k8s/v1/identity_service.py delete mode 100644 charms/glance-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py delete mode 100644 charms/glance-k8s/lib/charms/traefik_k8s/v2/ingress.py delete mode 100644 charms/glance-k8s/osci.yaml delete mode 100644 charms/glance-k8s/pyproject.toml delete mode 100755 charms/glance-k8s/rename.sh delete mode 100644 charms/glance-k8s/src/templates/parts/section-database delete mode 100644 charms/glance-k8s/src/templates/parts/section-federation delete mode 100644 charms/glance-k8s/src/templates/parts/section-identity delete mode 100644 charms/glance-k8s/src/templates/parts/section-middleware delete mode 100644 charms/glance-k8s/src/templates/parts/section-oslo-messaging-rabbit delete mode 100644 charms/glance-k8s/src/templates/parts/section-oslo-notifications delete mode 100644 charms/glance-k8s/src/templates/parts/section-service-user delete mode 100644 charms/glance-k8s/src/templates/parts/section-signing delete mode 100644 charms/glance-k8s/test-requirements.txt delete mode 100644 charms/glance-k8s/tox.ini delete mode 100644 charms/gnocchi-k8s/.gitignore delete mode 100644 charms/gnocchi-k8s/.gitreview delete mode 100644 charms/gnocchi-k8s/.stestr.conf delete mode 100644 charms/gnocchi-k8s/.zuul.yaml delete mode 100755 charms/gnocchi-k8s/fetch-libs.sh delete mode 100644 charms/gnocchi-k8s/lib/charms/data_platform_libs/v0/database_requires.py delete mode 100644 charms/gnocchi-k8s/lib/charms/gnocchi_k8s/v0/gnocchi_service.py delete mode 100644 charms/gnocchi-k8s/lib/charms/keystone_k8s/v1/identity_service.py delete mode 100644 charms/gnocchi-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py delete mode 100644 charms/gnocchi-k8s/lib/charms/traefik_k8s/v2/ingress.py delete mode 100644 charms/gnocchi-k8s/osci.yaml delete mode 100644 charms/gnocchi-k8s/pyproject.toml delete mode 100755 charms/gnocchi-k8s/rename.sh delete mode 100644 charms/gnocchi-k8s/src/templates/parts/database-connection delete mode 100644 charms/gnocchi-k8s/src/templates/parts/identity-data delete mode 100644 charms/gnocchi-k8s/src/templates/parts/section-database delete mode 100644 charms/gnocchi-k8s/src/templates/parts/section-federation delete mode 100644 charms/gnocchi-k8s/src/templates/parts/section-identity delete mode 100644 charms/gnocchi-k8s/src/templates/parts/section-middleware delete mode 100644 charms/gnocchi-k8s/src/templates/parts/section-signing delete mode 100644 charms/gnocchi-k8s/test-requirements.txt delete mode 100644 charms/gnocchi-k8s/tox.ini delete mode 100644 charms/heat-k8s/.gitignore delete mode 100644 charms/heat-k8s/.stestr.conf delete mode 100644 charms/heat-k8s/.zuul.yaml delete mode 100755 charms/heat-k8s/fetch-libs.sh delete mode 100644 charms/heat-k8s/lib/charms/data_platform_libs/v0/database_requires.py delete mode 100644 charms/heat-k8s/lib/charms/keystone_k8s/v1/identity_service.py delete mode 100644 charms/heat-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py delete mode 100644 charms/heat-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py delete mode 100644 charms/heat-k8s/lib/charms/traefik_k8s/v2/ingress.py delete mode 100644 charms/heat-k8s/osci.yaml delete mode 100644 charms/heat-k8s/pyproject.toml delete mode 100755 charms/heat-k8s/rename.sh delete mode 100644 charms/heat-k8s/src/templates/parts/section-database delete mode 100644 charms/heat-k8s/src/templates/parts/section-federation delete mode 100644 charms/heat-k8s/src/templates/parts/section-identity delete mode 100644 charms/heat-k8s/src/templates/parts/section-middleware delete mode 100644 charms/heat-k8s/src/templates/parts/section-oslo-messaging-rabbit delete mode 100644 charms/heat-k8s/src/templates/parts/section-signing delete mode 100644 charms/heat-k8s/test-requirements.txt delete mode 100644 charms/heat-k8s/tox.ini delete mode 100644 charms/horizon-k8s/.flake8 delete mode 100644 charms/horizon-k8s/.gitignore delete mode 100644 charms/horizon-k8s/.gitreview delete mode 100644 charms/horizon-k8s/.jujuignore delete mode 100644 charms/horizon-k8s/.stestr.conf delete mode 100644 charms/horizon-k8s/.zuul.yaml delete mode 100755 charms/horizon-k8s/fetch-libs.sh delete mode 100644 charms/horizon-k8s/lib/charms/data_platform_libs/v0/database_requires.py delete mode 100644 charms/horizon-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py delete mode 100644 charms/horizon-k8s/lib/charms/keystone_k8s/v1/cloud_credentials.py delete mode 100644 charms/horizon-k8s/lib/charms/keystone_k8s/v1/identity_service.py delete mode 100644 charms/horizon-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py delete mode 100644 charms/horizon-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py delete mode 100644 charms/horizon-k8s/lib/charms/traefik_k8s/v2/ingress.py delete mode 100644 charms/horizon-k8s/osci.yaml delete mode 100644 charms/horizon-k8s/pyproject.toml delete mode 100755 charms/horizon-k8s/rename.sh delete mode 100644 charms/horizon-k8s/src/templates/parts/database-connection delete mode 100644 charms/horizon-k8s/test-requirements.txt delete mode 100644 charms/horizon-k8s/tox.ini delete mode 100644 charms/keystone-k8s/.flake8 delete mode 100644 charms/keystone-k8s/.gitignore delete mode 100644 charms/keystone-k8s/.gitreview delete mode 100644 charms/keystone-k8s/.stestr.conf delete mode 100644 charms/keystone-k8s/.zuul.yaml delete mode 100755 charms/keystone-k8s/fetch-libs.sh delete mode 100644 charms/keystone-k8s/lib/charms/data_platform_libs/v0/database_requires.py delete mode 100644 charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py delete mode 100644 charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_resource.py delete mode 100644 charms/keystone-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py delete mode 100644 charms/keystone-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py delete mode 100644 charms/keystone-k8s/lib/charms/traefik_k8s/v2/ingress.py delete mode 100644 charms/keystone-k8s/osci.yaml delete mode 100644 charms/keystone-k8s/pyproject.toml delete mode 100755 charms/keystone-k8s/rename.sh delete mode 100644 charms/keystone-k8s/src/templates/parts/section-database delete mode 100644 charms/keystone-k8s/src/templates/parts/section-federation delete mode 100644 charms/keystone-k8s/src/templates/parts/section-middleware delete mode 100644 charms/keystone-k8s/src/templates/parts/section-oslo-messaging-rabbit delete mode 100644 charms/keystone-k8s/src/templates/parts/section-oslo-notifications delete mode 100644 charms/keystone-k8s/src/templates/parts/section-signing delete mode 100644 charms/keystone-k8s/test-requirements.txt delete mode 100644 charms/keystone-k8s/tox.ini delete mode 100644 charms/keystone-ldap-k8s/.gitignore delete mode 100644 charms/keystone-ldap-k8s/.gitreview delete mode 100644 charms/keystone-ldap-k8s/.stestr.conf delete mode 100644 charms/keystone-ldap-k8s/.zuul.yaml delete mode 100644 charms/keystone-ldap-k8s/lib/charms/keystone_k8s/v0/domain_config.py delete mode 100644 charms/keystone-ldap-k8s/osci.yaml delete mode 100644 charms/keystone-ldap-k8s/pyproject.toml delete mode 100755 charms/keystone-ldap-k8s/rename.sh delete mode 100644 charms/keystone-ldap-k8s/test-requirements.txt delete mode 100644 charms/keystone-ldap-k8s/tox.ini delete mode 100644 charms/magnum-k8s/.flake8 delete mode 100644 charms/magnum-k8s/.gitignore delete mode 100644 charms/magnum-k8s/.gitreview delete mode 100644 charms/magnum-k8s/.jujuignore delete mode 100644 charms/magnum-k8s/.stestr.conf delete mode 100644 charms/magnum-k8s/.zuul.yaml delete mode 100755 charms/magnum-k8s/fetch-libs.sh delete mode 100644 charms/magnum-k8s/lib/charms/data_platform_libs/v0/database_requires.py delete mode 100644 charms/magnum-k8s/lib/charms/keystone_k8s/v0/identity_resource.py delete mode 100644 charms/magnum-k8s/lib/charms/keystone_k8s/v1/identity_service.py delete mode 100644 charms/magnum-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py delete mode 100644 charms/magnum-k8s/lib/charms/traefik_k8s/v2/ingress.py delete mode 100644 charms/magnum-k8s/osci.yaml delete mode 100644 charms/magnum-k8s/pyproject.toml delete mode 100755 charms/magnum-k8s/rename.sh delete mode 100644 charms/magnum-k8s/src/templates/parts/database-connection delete mode 100644 charms/magnum-k8s/src/templates/parts/identity-data delete mode 100644 charms/magnum-k8s/src/templates/parts/section-database delete mode 100644 charms/magnum-k8s/src/templates/parts/section-identity delete mode 100644 charms/magnum-k8s/src/templates/parts/section-oslo-messaging-rabbit delete mode 100644 charms/magnum-k8s/src/templates/parts/section-service-user delete mode 100644 charms/magnum-k8s/test-requirements.txt delete mode 100644 charms/magnum-k8s/tox.ini delete mode 100644 charms/neutron-k8s/.flake8 delete mode 100644 charms/neutron-k8s/.gitignore delete mode 100644 charms/neutron-k8s/.gitreview delete mode 100644 charms/neutron-k8s/.jujuignore delete mode 100644 charms/neutron-k8s/.stestr.conf delete mode 100644 charms/neutron-k8s/.zuul.yaml delete mode 100755 charms/neutron-k8s/fetch-libs.sh delete mode 100644 charms/neutron-k8s/lib/charms/data_platform_libs/v0/database_requires.py delete mode 100644 charms/neutron-k8s/lib/charms/keystone_k8s/v1/identity_service.py delete mode 100644 charms/neutron-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py delete mode 100644 charms/neutron-k8s/lib/charms/traefik_k8s/v2/ingress.py delete mode 100644 charms/neutron-k8s/osci.yaml delete mode 100644 charms/neutron-k8s/pyproject.toml delete mode 100755 charms/neutron-k8s/rename.sh delete mode 100644 charms/neutron-k8s/src/templates/parts/section-database delete mode 100644 charms/neutron-k8s/src/templates/parts/section-federation delete mode 100644 charms/neutron-k8s/src/templates/parts/section-identity delete mode 100644 charms/neutron-k8s/src/templates/parts/section-middleware delete mode 100644 charms/neutron-k8s/src/templates/parts/section-oslo-messaging-rabbit delete mode 100644 charms/neutron-k8s/src/templates/parts/section-service-user delete mode 100644 charms/neutron-k8s/src/templates/parts/section-signing delete mode 100644 charms/neutron-k8s/test-requirements.txt delete mode 100644 charms/neutron-k8s/tox.ini delete mode 100644 charms/nova-k8s/.flake8 delete mode 100644 charms/nova-k8s/.gitignore delete mode 100644 charms/nova-k8s/.gitreview delete mode 100644 charms/nova-k8s/.jujuignore delete mode 100644 charms/nova-k8s/.stestr.conf delete mode 100644 charms/nova-k8s/.zuul.yaml delete mode 100755 charms/nova-k8s/fetch-libs.sh delete mode 100644 charms/nova-k8s/lib/charms/data_platform_libs/v0/database_requires.py delete mode 100644 charms/nova-k8s/lib/charms/keystone_k8s/v1/identity_service.py delete mode 100644 charms/nova-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py delete mode 100644 charms/nova-k8s/lib/charms/traefik_k8s/v2/ingress.py delete mode 100644 charms/nova-k8s/osci.yaml delete mode 100644 charms/nova-k8s/pyproject.toml delete mode 100755 charms/nova-k8s/rename.sh delete mode 100644 charms/nova-k8s/src/templates/parts/section-identity delete mode 100644 charms/nova-k8s/src/templates/parts/section-oslo-messaging-rabbit delete mode 100644 charms/nova-k8s/src/templates/parts/section-service-user delete mode 100644 charms/nova-k8s/test-requirements.txt delete mode 100644 charms/nova-k8s/tox.ini delete mode 100644 charms/octavia-k8s/.gitignore delete mode 100644 charms/octavia-k8s/.gitreview delete mode 100644 charms/octavia-k8s/.stestr.conf delete mode 100644 charms/octavia-k8s/.zuul.yaml delete mode 100755 charms/octavia-k8s/fetch-libs.sh delete mode 100644 charms/octavia-k8s/lib/charms/data_platform_libs/v0/database_requires.py delete mode 100644 charms/octavia-k8s/lib/charms/keystone_k8s/v0/identity_resource.py delete mode 100644 charms/octavia-k8s/lib/charms/keystone_k8s/v1/identity_service.py delete mode 100644 charms/octavia-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py delete mode 100644 charms/octavia-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py delete mode 100644 charms/octavia-k8s/lib/charms/traefik_k8s/v2/ingress.py delete mode 100644 charms/octavia-k8s/osci.yaml delete mode 100644 charms/octavia-k8s/pyproject.toml delete mode 100755 charms/octavia-k8s/rename.sh delete mode 100644 charms/octavia-k8s/src/templates/parts/database-connection delete mode 100644 charms/octavia-k8s/src/templates/parts/identity-data delete mode 100644 charms/octavia-k8s/src/templates/parts/section-database delete mode 100644 charms/octavia-k8s/src/templates/parts/section-federation delete mode 100644 charms/octavia-k8s/src/templates/parts/section-identity delete mode 100644 charms/octavia-k8s/src/templates/parts/section-middleware delete mode 100644 charms/octavia-k8s/src/templates/parts/section-signing delete mode 100644 charms/octavia-k8s/src/templates/parts/service-token delete mode 100644 charms/octavia-k8s/test-requirements.txt delete mode 100644 charms/octavia-k8s/tox.ini delete mode 100644 charms/openstack-exporter-k8s/.gitignore delete mode 100644 charms/openstack-exporter-k8s/.gitreview delete mode 100644 charms/openstack-exporter-k8s/.stestr.conf delete mode 100644 charms/openstack-exporter-k8s/.zuul.yaml delete mode 100644 charms/openstack-exporter-k8s/lib/charms/keystone_k8s/v0/identity_resource.py delete mode 100644 charms/openstack-exporter-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py delete mode 100644 charms/openstack-exporter-k8s/osci.yaml delete mode 100644 charms/openstack-exporter-k8s/pyproject.toml delete mode 100755 charms/openstack-exporter-k8s/rename.sh delete mode 100644 charms/openstack-exporter-k8s/test-requirements.txt delete mode 100644 charms/openstack-exporter-k8s/tox.ini delete mode 100644 charms/openstack-hypervisor/.gitignore delete mode 100644 charms/openstack-hypervisor/.gitreview delete mode 100644 charms/openstack-hypervisor/.stestr.conf delete mode 100644 charms/openstack-hypervisor/.zuul.yaml delete mode 100644 charms/openstack-hypervisor/lib/charms/ceilometer_k8s/v0/ceilometer_service.py delete mode 100644 charms/openstack-hypervisor/lib/charms/cinder_ceph_k8s/v0/ceph_access.py delete mode 100644 charms/openstack-hypervisor/lib/charms/data_platform_libs/v0/database_requires.py delete mode 100644 charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/cloud_credentials.py delete mode 100644 charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/identity_credentials.py delete mode 100644 charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/identity_service.py delete mode 100644 charms/openstack-hypervisor/lib/charms/keystone_k8s/v1/cloud_credentials.py delete mode 100644 charms/openstack-hypervisor/lib/charms/keystone_k8s/v1/identity_service.py delete mode 100644 charms/openstack-hypervisor/lib/charms/observability_libs/v0/kubernetes_service_patch.py delete mode 100644 charms/openstack-hypervisor/lib/charms/observability_libs/v1/kubernetes_service_patch.py delete mode 100644 charms/openstack-hypervisor/lib/charms/ovn_central_k8s/v0/ovsdb.py delete mode 100644 charms/openstack-hypervisor/lib/charms/rabbitmq_k8s/v0/rabbitmq.py delete mode 100644 charms/openstack-hypervisor/lib/charms/tls_certificates_interface/v1/tls_certificates.py delete mode 100644 charms/openstack-hypervisor/lib/charms/traefik_k8s/v1/ingress.py delete mode 100644 charms/openstack-hypervisor/osci.yaml delete mode 100644 charms/openstack-hypervisor/pyproject.toml delete mode 100755 charms/openstack-hypervisor/rename.sh delete mode 100644 charms/openstack-hypervisor/test-requirements.txt delete mode 100644 charms/openstack-hypervisor/tox.ini delete mode 100644 charms/ovn-central-k8s/.flake8 delete mode 100644 charms/ovn-central-k8s/.gitignore delete mode 100644 charms/ovn-central-k8s/.gitreview delete mode 100644 charms/ovn-central-k8s/.jujuignore delete mode 100644 charms/ovn-central-k8s/.stestr.conf delete mode 100644 charms/ovn-central-k8s/.zuul.yaml delete mode 100755 charms/ovn-central-k8s/fetch-libs.sh delete mode 100644 charms/ovn-central-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py delete mode 100644 charms/ovn-central-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py delete mode 100644 charms/ovn-central-k8s/osci.yaml delete mode 100644 charms/ovn-central-k8s/pyproject.toml delete mode 100755 charms/ovn-central-k8s/rename.sh delete mode 100644 charms/ovn-central-k8s/test-requirements.txt delete mode 100644 charms/ovn-central-k8s/tox.ini delete mode 100644 charms/ovn-relay-k8s/.flake8 delete mode 100644 charms/ovn-relay-k8s/.gitignore delete mode 100644 charms/ovn-relay-k8s/.gitreview delete mode 100644 charms/ovn-relay-k8s/.jujuignore delete mode 100644 charms/ovn-relay-k8s/.stestr.conf delete mode 100644 charms/ovn-relay-k8s/.zuul.yaml delete mode 100755 charms/ovn-relay-k8s/fetch-libs.sh delete mode 100644 charms/ovn-relay-k8s/lib/charms/observability_libs/v1/kubernetes_service_patch.py delete mode 100644 charms/ovn-relay-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py delete mode 100644 charms/ovn-relay-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py delete mode 100644 charms/ovn-relay-k8s/osci.yaml delete mode 100644 charms/ovn-relay-k8s/pyproject.toml delete mode 100755 charms/ovn-relay-k8s/rename.sh delete mode 100644 charms/ovn-relay-k8s/test-requirements.txt delete mode 100644 charms/ovn-relay-k8s/tox.ini delete mode 100644 charms/placement-k8s/.flake8 delete mode 100644 charms/placement-k8s/.gitignore delete mode 100644 charms/placement-k8s/.gitreview delete mode 100644 charms/placement-k8s/.jujuignore delete mode 100644 charms/placement-k8s/.stestr.conf delete mode 100644 charms/placement-k8s/.zuul.yaml delete mode 100755 charms/placement-k8s/fetch-libs.sh delete mode 100644 charms/placement-k8s/lib/charms/data_platform_libs/v0/database_requires.py delete mode 100644 charms/placement-k8s/lib/charms/keystone_k8s/v1/identity_service.py delete mode 100644 charms/placement-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py delete mode 100644 charms/placement-k8s/lib/charms/traefik_k8s/v2/ingress.py delete mode 100644 charms/placement-k8s/osci.yaml delete mode 100644 charms/placement-k8s/pyproject.toml delete mode 100755 charms/placement-k8s/rename.sh delete mode 100644 charms/placement-k8s/src/templates/parts/section-database delete mode 100644 charms/placement-k8s/src/templates/parts/section-federation delete mode 100644 charms/placement-k8s/src/templates/parts/section-identity delete mode 100644 charms/placement-k8s/src/templates/parts/section-middleware delete mode 100644 charms/placement-k8s/src/templates/parts/section-service-user delete mode 100644 charms/placement-k8s/src/templates/parts/section-signing delete mode 100644 charms/placement-k8s/test-requirements.txt delete mode 100644 charms/placement-k8s/tox.ini create mode 100644 common.sh create mode 100755 fetch_libs.sh rename {charms/cinder-ceph-k8s => libs/external}/lib/charms/data_platform_libs/v0/database_requires.py (100%) rename {charms/openstack-hypervisor => libs/external}/lib/charms/grafana_agent/v0/cos_agent.py (100%) rename {charms/openstack-exporter-k8s => libs/external}/lib/charms/grafana_k8s/v0/grafana_dashboard.py (100%) rename {charms/designate-bind-k8s => libs/external}/lib/charms/observability_libs/v1/kubernetes_service_patch.py (100%) rename {charms/openstack-hypervisor => libs/external}/lib/charms/operator_libs_linux/v2/snap.py (100%) rename {charms/openstack-exporter-k8s => libs/external}/lib/charms/prometheus_k8s/v0/prometheus_scrape.py (100%) rename {charms/aodh-k8s => libs/external}/lib/charms/rabbitmq_k8s/v0/rabbitmq.py (100%) rename {charms/neutron-k8s => libs/external}/lib/charms/tls_certificates_interface/v1/tls_certificates.py (100%) rename {charms/aodh-k8s => libs/external}/lib/charms/traefik_k8s/v2/ingress.py (100%) rename {charms/heat-k8s => libs/external}/lib/charms/traefik_route_k8s/v0/traefik_route.py (100%) rename {charms/barbican-k8s => libs/external}/lib/charms/vault_k8s/v0/vault_kv.py (100%) rename {charms/ceilometer-k8s => libs/internal}/lib/charms/ceilometer_k8s/v0/ceilometer_service.py (100%) rename {charms/cinder-ceph-k8s => libs/internal}/lib/charms/cinder_ceph_k8s/v0/ceph_access.py (100%) rename {charms/cinder-ceph-k8s => libs/internal}/lib/charms/cinder_k8s/v0/storage_backend.py (100%) rename {charms/designate-bind-k8s => libs/internal}/lib/charms/designate_bind_k8s/v0/bind_rndc.py (100%) rename {charms/ceilometer-k8s => libs/internal}/lib/charms/gnocchi_k8s/v0/gnocchi_service.py (100%) rename {charms/gnocchi-k8s => libs/internal}/lib/charms/gnocchi_k8s/v0/metric_service.py (100%) rename {charms/keystone-k8s => libs/internal}/lib/charms/keystone_k8s/v0/cloud_credentials.py (100%) rename {charms/keystone-k8s => libs/internal}/lib/charms/keystone_k8s/v0/domain_config.py (100%) rename {charms/ceilometer-k8s => libs/internal}/lib/charms/keystone_k8s/v0/identity_credentials.py (100%) rename {charms/heat-k8s => libs/internal}/lib/charms/keystone_k8s/v0/identity_resource.py (100%) rename {charms/keystone-k8s => libs/internal}/lib/charms/keystone_k8s/v0/identity_service.py (100%) rename {charms/keystone-k8s => libs/internal}/lib/charms/keystone_k8s/v1/cloud_credentials.py (100%) rename {charms/keystone-k8s => libs/internal}/lib/charms/keystone_k8s/v1/identity_service.py (100%) rename {charms/neutron-k8s => libs/internal}/lib/charms/ovn_central_k8s/v0/ovsdb.py (100%) rename {charms/nova-k8s => libs/internal}/lib/charms/sunbeam_nova_compute_operator/v0/cloud_compute.py (100%) delete mode 100644 ops-sunbeam/pyproject.toml delete mode 100644 ops-sunbeam/test-requirements.txt delete mode 100644 ops-sunbeam/tox.ini create mode 100644 playbooks/charm/build.yaml create mode 100644 playbooks/charm/publish.yaml create mode 100644 playbooks/collect-run-data.yaml create mode 100644 playbooks/zaza-func-test.yaml rename charms/aodh-k8s/pyproject.toml => pyproject.toml (100%) create mode 100644 render_bundles.py create mode 100644 roles/charm-build/tasks/main.yaml create mode 100644 roles/charm-publish/defaults/main.yaml create mode 100644 roles/charm-publish/tasks/main.yaml create mode 100644 roles/charm-publish/tasks/publish.yaml create mode 100644 roles/collect-run-data/tasks/main.yaml create mode 100644 roles/microk8s-cloud/tasks/main.yaml create mode 100644 roles/microk8s-cloud/templates/hosts.j2 create mode 100644 roles/zaza-func-test/tasks/main.yaml create mode 100755 run_tox.sh rename {charms/aodh-k8s/src/templates => templates}/parts/database-connection (55%) rename {charms/aodh-k8s/src/templates => templates}/parts/identity-data (100%) rename {charms/ceilometer-k8s/src/templates => templates}/parts/identity-data-id-creds (100%) rename {charms/magnum-k8s/src/templates => templates}/parts/section-certificates (100%) rename {charms/aodh-k8s/src/templates => templates}/parts/section-database (100%) rename {charms/aodh-k8s/src/templates => templates}/parts/section-federation (100%) rename {charms/aodh-k8s/src/templates => templates}/parts/section-identity (100%) rename {charms/aodh-k8s/src/templates => templates}/parts/section-middleware (100%) rename {charms/keystone-k8s/src/templates => templates}/parts/section-oslo-cache (100%) rename {charms/aodh-k8s/src/templates => templates}/parts/section-oslo-messaging-rabbit (100%) rename {charms/keystone-k8s/src/templates => templates}/parts/section-oslo-middleware (100%) rename {charms/cinder-ceph-k8s/src/templates => templates}/parts/section-oslo-notifications (100%) rename {charms/aodh-k8s/src/templates => templates}/parts/section-service-credentials (100%) rename charms/ceilometer-k8s/src/templates/parts/section-service-credentials => templates/parts/section-service-credentials-from-identity-service (100%) rename {charms/barbican-k8s/src/templates => templates}/parts/section-service-user (100%) rename charms/ceilometer-k8s/src/templates/parts/section-service-user-id-creds => templates/parts/section-service-user-from-identity-credentials (100%) rename {charms/aodh-k8s/src/templates => templates}/parts/section-signing (100%) rename {charms/magnum-k8s/src/templates => templates}/parts/section-trust (100%) create mode 100644 test-requirements.txt create mode 100644 tests/caas/smoke.yaml.j2 create mode 100644 tests/caas/tests.yaml create mode 100644 tests/ceph/smoke.yaml.j2 create mode 100644 tests/ceph/tests.yaml create mode 100644 tests/core/smoke.yaml.j2 create mode 100644 tests/core/tests.yaml create mode 100644 tests/misc/smoke.yaml.j2 create mode 100644 tests/misc/tests.yaml create mode 100644 tox.ini create mode 100644 zuul.d/jobs.yaml create mode 100644 zuul.d/project-templates.yaml create mode 100644 zuul.d/secrets.yaml create mode 100644 zuul.d/zuul.yaml diff --git a/charms/aodh-k8s/.gitignore b/.gitignore similarity index 84% rename from charms/aodh-k8s/.gitignore rename to .gitignore index 24ff2e41..32543ded 100644 --- a/charms/aodh-k8s/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ venv/ build/ +.idea/ +.vscode/ *.charm .tox/ .coverage __pycache__/ *.py[cod] -.idea -.vscode/ -*.swp +**.swp .stestr/ diff --git a/charms/heat-k8s/.gitreview b/.gitreview similarity index 63% rename from charms/heat-k8s/.gitreview rename to .gitreview index 5afeb9d1..acf6b77a 100644 --- a/charms/heat-k8s/.gitreview +++ b/.gitreview @@ -1,5 +1,5 @@ [gerrit] host=review.opendev.org port=29418 -project=openstack/charm-heat-k8s.git +project=openstack/sunbeam-charms.git defaultbranch=main diff --git a/charms/keystone-k8s/.jujuignore b/.jujuignore similarity index 100% rename from charms/keystone-k8s/.jujuignore rename to .jujuignore diff --git a/charms/aodh-k8s/.stestr.conf b/.stestr.conf similarity index 100% rename from charms/aodh-k8s/.stestr.conf rename to .stestr.conf diff --git a/charms/aodh-k8s/.gitreview b/charms/aodh-k8s/.gitreview deleted file mode 100644 index c7cbdfc4..00000000 --- a/charms/aodh-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-aodh-k8s.git -defaultbranch=main diff --git a/charms/aodh-k8s/.zuul.yaml b/charms/aodh-k8s/.zuul.yaml deleted file mode 100644 index 87b6ace0..00000000 --- a/charms/aodh-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: aodh-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/aodh-k8s/charmcraft.yaml b/charms/aodh-k8s/charmcraft.yaml index 2fdc318d..9556149e 100644 --- a/charms/aodh-k8s/charmcraft.yaml +++ b/charms/aodh-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/aodh-k8s/fetch-libs.sh b/charms/aodh-k8s/fetch-libs.sh deleted file mode 100755 index e7772471..00000000 --- a/charms/aodh-k8s/fetch-libs.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.data_platform_libs.v0.database_requires -charmcraft fetch-lib charms.keystone_k8s.v1.identity_service -charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.traefik_k8s.v2.ingress diff --git a/charms/aodh-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/aodh-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 11ffd6ca..00000000 --- a/charms/aodh-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,537 +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. - -r"""[DEPRECATED] Relation 'requires' side abstraction for database relation. - -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.database_requires 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.database_requires 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, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 = 6 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(RelationEvent): - """Base class for database events.""" - - @property - def endpoints(self) -> Optional[str]: - """Returns a comma separated list of read/write endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("endpoints") - - @property - def password(self) -> Optional[str]: - """Returns the password for the created user.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("password") - - @property - def read_only_endpoints(self) -> Optional[str]: - """Returns a comma separated list of read only endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("read-only-endpoints") - - @property - def replset(self) -> Optional[str]: - """Returns the replicaset name. - - MongoDB only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("replset") - - @property - def tls(self) -> Optional[str]: - """Returns whether TLS is configured.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls") - - @property - def tls_ca(self) -> Optional[str]: - """Returns TLS CA.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls-ca") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("username") - - @property - def version(self) -> Optional[str]: - """Returns the version of the database. - - Version as informed by the database daemon. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("version") - - -class DatabaseCreatedEvent(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(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) - - -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 — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() # pyright: ignore [reportGeneralTypeIssues] - - def __init__( - self, - charm, - relation_name: str, - database_name: str, - extra_user_roles: Optional[str] = None, - relations_aliases: Optional[List[str]] = None, - ): - """Manager of database client relations.""" - super().__init__(charm, relation_name) - self.charm = charm - self.database = database_name - 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.relations_aliases = relations_aliases - 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 - ) - - # 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 _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. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].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"} - if event.app - else {} - ) - - # 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] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - 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 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"} - if relation.app - else {} - ) - 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 _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()) - getattr(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()) - getattr(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()) - getattr(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") - - @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]) diff --git a/charms/aodh-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/aodh-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3f..00000000 --- a/charms/aodh-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -Also provide additional parameters to the charm object: - - service - - internal_url - - public_url - - admin_url - - region - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - service = "my-service" - internal_url = "http://internal-url" - public_url = "http://public-url" - admin_url = "http://admin-url" - region = "region" - ) - self.framework.observe( - self.identity_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# 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 - - -logger = logging.getLogger(__name__) - - -class IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._identity_service_rel.data[self._identity_service_rel.app] - return data.get(key) - - @property - def api_version(self) -> str: - """Return the api_version.""" - return self.get_remote_app_data('api-version') - - @property - def auth_host(self) -> str: - """Return the auth_host.""" - return self.get_remote_app_data('auth-host') - - @property - def auth_port(self) -> str: - """Return the auth_port.""" - return self.get_remote_app_data('auth-port') - - @property - def auth_protocol(self) -> str: - """Return the auth_protocol.""" - return self.get_remote_app_data('auth-protocol') - - @property - def internal_host(self) -> str: - """Return the internal_host.""" - return self.get_remote_app_data('internal-host') - - @property - def internal_port(self) -> str: - """Return the internal_port.""" - return self.get_remote_app_data('internal-port') - - @property - def internal_protocol(self) -> str: - """Return the internal_protocol.""" - return self.get_remote_app_data('internal-protocol') - - @property - def admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("password") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("username") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_credentials(self, relation_name: int, - relation_id: str, - api_version: str, - auth_host: str, - auth_port: str, - auth_protocol: str, - internal_host: str, - internal_port: str, - internal_protocol: str, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_rel.data[self.charm.app] - app_data["api-version"] = api_version - app_data["auth-host"] = auth_host - app_data["auth-port"] = str(auth_port) - app_data["auth-protocol"] = auth_protocol - app_data["internal-host"] = internal_host - app_data["internal-port"] = str(internal_port) - app_data["internal-protocol"] = internal_protocol - app_data["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/charms/aodh-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py b/charms/aodh-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py deleted file mode 100644 index 4cf26164..00000000 --- a/charms/aodh-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py +++ /dev/null @@ -1,408 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# Licensed under the Apache2.0, see LICENCE file in charm source for details. -"""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: -- additional-hostnames -- backend-protocol -- limit-rps -- limit-whitelist -- max-body-size -- owasp-modsecurity-crs -- owasp-modsecurity-custom-rules -- path-routes -- retry-errors -- rewrite-enabled -- rewrite-target -- service-hostname (required) -- service-name (required) -- service-namespace -- service-port (required) -- 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, for the relation -changed event to be properly handled. -""" - -import copy -import logging -from typing import Dict - -from ops.charm import CharmBase, CharmEvents, RelationBrokenEvent, RelationChangedEvent -from ops.framework import EventBase, EventSource, Object -from ops.model import BlockedStatus - -INGRESS_RELATION_NAME = "ingress" -INGRESS_PROXY_RELATION_NAME = "ingress-proxy" - -# 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 = 16 - -LOGGER = logging.getLogger(__name__) - -REQUIRED_INGRESS_RELATION_FIELDS = {"service-hostname", "service-name", "service-port"} - -OPTIONAL_INGRESS_RELATION_FIELDS = { - "additional-hostnames", - "backend-protocol", - "limit-rps", - "limit-whitelist", - "max-body-size", - "owasp-modsecurity-crs", - "owasp-modsecurity-custom-rules", - "path-routes", - "retry-errors", - "rewrite-target", - "rewrite-enabled", - "service-namespace", - "session-cookie-max-age", - "tls-secret-name", -} - -RELATION_INTERFACES_MAPPINGS = { - "service-hostname": "host", - "service-name": "name", - "service-namespace": "model", - "service-port": "port", -} -RELATION_INTERFACES_MAPPINGS_VALUES = set(RELATION_INTERFACES_MAPPINGS.values()) - - -class IngressAvailableEvent(EventBase): - """IngressAvailableEvent custom event. - - This event indicates the Ingress provider is available. - """ - - -class IngressProxyAvailableEvent(EventBase): - """IngressProxyAvailableEvent custom event. - - This event indicates the IngressProxy provider is available. - """ - - -class IngressBrokenEvent(RelationBrokenEvent): - """IngressBrokenEvent custom event. - - This event indicates the Ingress provider is broken. - """ - - -class IngressCharmEvents(CharmEvents): - """Custom charm events. - - Attrs: - ingress_available: Event to indicate that Ingress is available. - ingress_proxy_available: Event to indicate that IngressProxy is available. - ingress_broken: Event to indicate that Ingress is broken. - """ - - ingress_available = EventSource(IngressAvailableEvent) - ingress_proxy_available = EventSource(IngressProxyAvailableEvent) - 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 - - Attrs: - model: Juju model where the charm is deployed. - config_dict: Contains all the configuration options for Ingress. - """ - - def __init__(self, charm: CharmBase, config_dict: Dict) -> None: - """Init function for the IngressRequires class. - - Args: - charm: The charm that requires the ingress relation. - config_dict: Contains all the configuration options for Ingress. - """ - super().__init__(charm, INGRESS_RELATION_NAME) - - self.framework.observe( - charm.on[INGRESS_RELATION_NAME].relation_changed, self._on_relation_changed - ) - - # Set default values. - default_relation_fields = { - "service-namespace": self.model.name, - } - config_dict.update( - (key, value) - for key, value in default_relation_fields.items() - if key not in config_dict or not config_dict[key] - ) - - self.config_dict = self._convert_to_relation_interface(config_dict) - - @staticmethod - def _convert_to_relation_interface(config_dict: Dict) -> Dict: - """Create a new relation dict that conforms with charm-relation-interfaces. - - Args: - config_dict: Ingress configuration that doesn't conform with charm-relation-interfaces. - - Returns: - The Ingress configuration conforming with charm-relation-interfaces. - """ - config_dict = copy.copy(config_dict) - config_dict.update( - (key, config_dict[old_key]) - for old_key, key in RELATION_INTERFACES_MAPPINGS.items() - if old_key in config_dict and config_dict[old_key] - ) - return config_dict - - def _config_dict_errors(self, config_dict: Dict, update_only: bool = False) -> bool: - """Check our config dict for errors. - - Args: - config_dict: Contains all the configuration options for Ingress. - update_only: If the charm needs to update only existing keys. - - Returns: - If we need to update the config dict or not. - """ - blocked_message = "Error in ingress relation, check `juju debug-log`" - unknown = [ - config_key - for config_key in config_dict - if config_key - not in REQUIRED_INGRESS_RELATION_FIELDS - | OPTIONAL_INGRESS_RELATION_FIELDS - | RELATION_INTERFACES_MAPPINGS_VALUES - ] - 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 = tuple( - config_key - for config_key in REQUIRED_INGRESS_RELATION_FIELDS - if config_key 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: RelationChangedEvent) -> None: - """Handle the relation-changed event. - - Args: - event: Event triggering the relation-changed hook for the relation. - """ - # `self.unit` isn't available here, so use `self.model.unit`. - if self.model.unit.is_leader(): - if self._config_dict_errors(config_dict=self.config_dict): - return - event.relation.data[self.model.app].update( - (key, str(self.config_dict[key])) for key in self.config_dict - ) - - def update_config(self, config_dict: Dict) -> None: - """Allow for updates to relation. - - Args: - config_dict: Contains all the configuration options for Ingress. - - Attrs: - config_dict: Contains all the configuration options for Ingress. - """ - if self.model.unit.is_leader(): - self.config_dict = self._convert_to_relation_interface(config_dict) - if self._config_dict_errors(self.config_dict, update_only=True): - return - relation = self.model.get_relation(INGRESS_RELATION_NAME) - if relation: - for key in self.config_dict: - relation.data[self.model.app][key] = str(self.config_dict[key]) - - -class IngressBaseProvides(Object): - """Parent class for IngressProvides and IngressProxyProvides. - - Attrs: - model: Juju model where the charm is deployed. - """ - - def __init__(self, charm: CharmBase, relation_name: str) -> None: - """Init function for the IngressProxyProvides class. - - Args: - charm: The charm that provides the ingress-proxy relation. - """ - super().__init__(charm, relation_name) - self.charm = charm - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handle a change to the ingress/ingress-proxy relation. - - Confirm we have the fields we expect to receive. - - Args: - event: Event triggering the relation-changed hook for the relation. - """ - # `self.unit` isn't available here, so use `self.model.unit`. - if not self.model.unit.is_leader(): - return - - relation_name = event.relation.name - - assert event.app is not None # nosec - if not event.relation.data[event.app]: - LOGGER.info( - "%s hasn't finished configuring, waiting until relation is changed again.", - relation_name, - ) - 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.warning( - "Missing required data fields for %s relation: %s", - relation_name, - ", ".join(missing_fields), - ) - self.model.unit.status = BlockedStatus( - f"Missing fields for {relation_name}: {', '.join(missing_fields)}" - ) - - if relation_name == INGRESS_RELATION_NAME: - # Conform to charm-relation-interfaces. - if "name" in ingress_data and "port" in ingress_data: - name = ingress_data["name"] - port = ingress_data["port"] - else: - name = ingress_data["service-name"] - port = ingress_data["service-port"] - event.relation.data[self.model.app]["url"] = f"http://{name}:{port}/" - - # Create an event that our charm can use to decide it's okay to - # configure the ingress. - self.charm.on.ingress_available.emit() - elif relation_name == INGRESS_PROXY_RELATION_NAME: - self.charm.on.ingress_proxy_available.emit() - - -class IngressProvides(IngressBaseProvides): - """Class containing the functionality for the 'provides' side of the 'ingress' relation. - - Attrs: - charm: The charm that provides the ingress relation. - - Hook events observed: - - relation-changed - """ - - def __init__(self, charm: CharmBase) -> None: - """Init function for the IngressProvides class. - - Args: - charm: The charm that provides the ingress relation. - """ - super().__init__(charm, INGRESS_RELATION_NAME) - # Observe the relation-changed hook event and bind - # self.on_relation_changed() to handle the event. - self.framework.observe( - charm.on[INGRESS_RELATION_NAME].relation_changed, self._on_relation_changed - ) - self.framework.observe( - charm.on[INGRESS_RELATION_NAME].relation_broken, self._on_relation_broken - ) - - def _on_relation_broken(self, event: RelationBrokenEvent) -> None: - """Handle a relation-broken event in the ingress relation. - - Args: - event: Event triggering the relation-broken hook for the 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(event.relation) - - -class IngressProxyProvides(IngressBaseProvides): - """Class containing the functionality for the 'provides' side of the 'ingress-proxy' relation. - - Attrs: - charm: The charm that provides the ingress-proxy relation. - - Hook events observed: - - relation-changed - """ - - def __init__(self, charm: CharmBase) -> None: - """Init function for the IngressProxyProvides class. - - Args: - charm: The charm that provides the ingress-proxy relation. - """ - super().__init__(charm, INGRESS_PROXY_RELATION_NAME) - # Observe the relation-changed hook event and bind - # self.on_relation_changed() to handle the event. - self.framework.observe( - charm.on[INGRESS_PROXY_RELATION_NAME].relation_changed, self._on_relation_changed - ) diff --git a/charms/aodh-k8s/osci.yaml b/charms/aodh-k8s/osci.yaml deleted file mode 100644 index b59f7058..00000000 --- a/charms/aodh-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: aodh-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/aodh-k8s/rename.sh b/charms/aodh-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/aodh-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/aodh-k8s/requirements.txt b/charms/aodh-k8s/requirements.txt index 5806e426..1dcec118 100644 --- a/charms/aodh-k8s/requirements.txt +++ b/charms/aodh-k8s/requirements.txt @@ -1,9 +1,11 @@ ops jinja2 -git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam lightkube pydantic<2.0 # Uncomment below if charm relates to ceph # git+https://github.com/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client # git+https://github.com/juju/charm-helpers.git#egg=charmhelpers + +# From ops_sunbeam +tenacity diff --git a/charms/aodh-k8s/src/templates/parts/section-service-user b/charms/aodh-k8s/src/templates/parts/section-service-user deleted file mode 100644 index 165fbe71..00000000 --- a/charms/aodh-k8s/src/templates/parts/section-service-user +++ /dev/null @@ -1,15 +0,0 @@ -{% if identity_service.service_domain_id -%} -[service_user] -{% if identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -send_service_user_token = true -auth_type = password -project_domain_id = {{ identity_service.service_domain_id }} -user_domain_id = {{ identity_service.service_domain_id }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -{% endif -%} diff --git a/charms/aodh-k8s/test-requirements.txt b/charms/aodh-k8s/test-requirements.txt deleted file mode 100644 index 276e5bee..00000000 --- a/charms/aodh-k8s/test-requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -# This file is managed centrally. If you find the need to modify this as a -# one-off, please don't. Intead, consult #openstack-charms and ask about -# requirements management in charms via bot-control. Thank you. - -coverage -mock -flake8 -stestr -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/aodh-k8s/tests/unit/test_charm.py b/charms/aodh-k8s/tests/unit/test_charm.py index a5967b30..0329067b 100644 --- a/charms/aodh-k8s/tests/unit/test_charm.py +++ b/charms/aodh-k8s/tests/unit/test_charm.py @@ -16,9 +16,8 @@ """Tests for gnocchi charm.""" -import ops_sunbeam.test_utils as test_utils - import charm +import ops_sunbeam.test_utils as test_utils class _AodhOperatorCharm(charm.AodhOperatorCharm): diff --git a/charms/aodh-k8s/tox.ini b/charms/aodh-k8s/tox.ini deleted file mode 100644 index 067963f9..00000000 --- a/charms/aodh-k8s/tox.ini +++ /dev/null @@ -1,165 +0,0 @@ -# Operator charm (with zaza): tox.ini - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - HOME - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/barbican-k8s/.flake8 b/charms/barbican-k8s/.flake8 deleted file mode 100644 index 8ef84fcd..00000000 --- a/charms/barbican-k8s/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 99 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/charms/barbican-k8s/.gitignore b/charms/barbican-k8s/.gitignore deleted file mode 100644 index 73f116c9..00000000 --- a/charms/barbican-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -*.charm -*.swp - -.coverage -__pycache__/ -*.py[cod] -.tox -.stestr/ -tempest.log diff --git a/charms/barbican-k8s/.gitreview b/charms/barbican-k8s/.gitreview deleted file mode 100644 index dbfd1ea0..00000000 --- a/charms/barbican-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-barbican-k8s.git -defaultbranch=main diff --git a/charms/barbican-k8s/.jujuignore b/charms/barbican-k8s/.jujuignore deleted file mode 100644 index 6ccd559e..00000000 --- a/charms/barbican-k8s/.jujuignore +++ /dev/null @@ -1,3 +0,0 @@ -/venv -*.py[cod] -*.charm diff --git a/charms/barbican-k8s/.stestr.conf b/charms/barbican-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/barbican-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/barbican-k8s/.zuul.yaml b/charms/barbican-k8s/.zuul.yaml deleted file mode 100644 index c7711b8f..00000000 --- a/charms/barbican-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: barbican-k8s - juju_channel: 3.2/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/barbican-k8s/charmcraft.yaml b/charms/barbican-k8s/charmcraft.yaml index 58193c73..de20145a 100644 --- a/charms/barbican-k8s/charmcraft.yaml +++ b/charms/barbican-k8s/charmcraft.yaml @@ -20,4 +20,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/barbican-k8s/fetch-libs.sh b/charms/barbican-k8s/fetch-libs.sh deleted file mode 100755 index a573dd64..00000000 --- a/charms/barbican-k8s/fetch-libs.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.data_platform_libs.v0.database_requires -charmcraft fetch-lib charms.keystone_k8s.v1.identity_service -charmcraft fetch-lib charms.keystone_k8s.v0.identity_resource -charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.traefik_k8s.v2.ingress diff --git a/charms/barbican-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/barbican-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 11ffd6ca..00000000 --- a/charms/barbican-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,537 +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. - -r"""[DEPRECATED] Relation 'requires' side abstraction for database relation. - -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.database_requires 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.database_requires 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, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 = 6 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(RelationEvent): - """Base class for database events.""" - - @property - def endpoints(self) -> Optional[str]: - """Returns a comma separated list of read/write endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("endpoints") - - @property - def password(self) -> Optional[str]: - """Returns the password for the created user.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("password") - - @property - def read_only_endpoints(self) -> Optional[str]: - """Returns a comma separated list of read only endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("read-only-endpoints") - - @property - def replset(self) -> Optional[str]: - """Returns the replicaset name. - - MongoDB only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("replset") - - @property - def tls(self) -> Optional[str]: - """Returns whether TLS is configured.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls") - - @property - def tls_ca(self) -> Optional[str]: - """Returns TLS CA.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls-ca") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("username") - - @property - def version(self) -> Optional[str]: - """Returns the version of the database. - - Version as informed by the database daemon. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("version") - - -class DatabaseCreatedEvent(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(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) - - -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 — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() # pyright: ignore [reportGeneralTypeIssues] - - def __init__( - self, - charm, - relation_name: str, - database_name: str, - extra_user_roles: Optional[str] = None, - relations_aliases: Optional[List[str]] = None, - ): - """Manager of database client relations.""" - super().__init__(charm, relation_name) - self.charm = charm - self.database = database_name - 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.relations_aliases = relations_aliases - 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 - ) - - # 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 _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. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].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"} - if event.app - else {} - ) - - # 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] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - 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 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"} - if relation.app - else {} - ) - 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 _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()) - getattr(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()) - getattr(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()) - getattr(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") - - @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]) diff --git a/charms/barbican-k8s/lib/charms/keystone_k8s/v0/identity_resource.py b/charms/barbican-k8s/lib/charms/keystone_k8s/v0/identity_resource.py deleted file mode 100644 index 154fab83..00000000 --- a/charms/barbican-k8s/lib/charms/keystone_k8s/v0/identity_resource.py +++ /dev/null @@ -1,392 +0,0 @@ -"""IdentityResourceProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the identity_ops interface. - -Import `IdentityResourceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_ops" - -Also provide additional parameters to the charm object: - - request - -Three events are also available to respond to: - - provider_ready - - provider_goneaway - - response_avaialable - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v0.identity_resource import IdentityResourceRequires - -class IdentityResourceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityResource Requires - self.identity_resource = IdentityResourceRequires( - self, "identity_ops", - ) - self.framework.observe( - self.identity_resource.on.provider_ready, self._on_identity_resource_ready) - self.framework.observe( - self.identity_resource.on.provider_goneaway, self._on_identity_resource_goneaway) - self.framework.observe( - self.identity_resource.on.response_available, self._on_identity_resource_response) - - def _on_identity_resource_ready(self, event): - '''React to the IdentityResource provider_ready event. - - This event happens when n IdentityResource relation is added to the - model. Ready to send any ops to keystone. - ''' - # Ready to send any ops. - pass - - def _on_identity_resource_response(self, event): - '''React to the IdentityResource response_available event. - - The IdentityResource interface will provide the response for the ops sent. - ''' - # Read the response for the ops sent. - pass - - def _on_identity_resource_goneaway(self, event): - '''React to the IdentityResource goneaway event. - - This event happens when an IdentityResource relation is removed. - ''' - # IdentityResource Relation has goneaway. No ops can be sent. - pass -``` - -A sample ops request can be of format -{ - "id": - "tag": - "ops": [ - { - "name": , - "params": { - : , - : - } - } - ] -} - -For any sensitive data in the ops params, the charm can create secrets and pass -secret id instead of sensitive data as part of ops request. The charm should -ensure to grant secret access to provider charm i.e., keystone over relation. -The secret content should hold the sensitive data with same name as param name. -""" - -import json -import logging -from typing import ( - Optional, -) - -from ops.charm import ( - CharmBase, - RelationBrokenEvent, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import ( - EventBase, - EventSource, - Object, - ObjectEvents, - StoredState, -) -from ops.model import ( - Relation, -) - -logger = logging.getLogger(__name__) - - -# The unique Charmhub library identifier, never change it -LIBID = "b419d4d8249e423487daafc3665ed06f" - -# 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 - - -REQUEST_NOT_SENT = 1 -REQUEST_SENT = 2 -REQUEST_PROCESSED = 3 - - -class IdentityOpsProviderReadyEvent(RelationEvent): - """Has IdentityOpsProviderReady Event.""" - - pass - - -class IdentityOpsResponseEvent(RelationEvent): - """Has IdentityOpsResponse Event.""" - - pass - - -class IdentityOpsProviderGoneAwayEvent(RelationEvent): - """Has IdentityOpsProviderGoneAway Event.""" - - pass - - -class IdentityResourceResponseEvents(ObjectEvents): - """Events class for `on`.""" - - provider_ready = EventSource(IdentityOpsProviderReadyEvent) - response_available = EventSource(IdentityOpsResponseEvent) - provider_goneaway = EventSource(IdentityOpsProviderGoneAwayEvent) - - -class IdentityResourceRequires(Object): - """IdentityResourceRequires class.""" - - on = IdentityResourceResponseEvents() - _stored = StoredState() - - def __init__(self, charm: CharmBase, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self._stored.set_default(provider_ready=False, requests=[]) - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_resource_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_resource_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_resource_relation_broken, - ) - - def _on_identity_resource_relation_joined( - self, event: RelationJoinedEvent - ): - """Handle IdentityResource joined.""" - self._stored.provider_ready = True - self.on.provider_ready.emit(event.relation) - - def _on_identity_resource_relation_changed( - self, event: RelationChangedEvent - ): - """Handle IdentityResource changed.""" - id_ = self.response.get("id") - self.save_request_in_store(id_, None, None, REQUEST_PROCESSED) - self.on.response_available.emit(event.relation) - - def _on_identity_resource_relation_broken( - self, event: RelationBrokenEvent - ): - """Handle IdentityResource broken.""" - self._stored.provider_ready = False - self.on.provider_goneaway.emit(event.relation) - - @property - def _identity_resource_rel(self) -> Optional[Relation]: - """The IdentityResource relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def response(self) -> dict: - """Response object from keystone.""" - response = self.get_remote_app_data("response") - if not response: - return {} - - try: - return json.loads(response) - except Exception as e: - logger.debug(str(e)) - - return {} - - def save_request_in_store( - self, id: str, tag: str, ops: list, state: int - ) -> None: - """Save request in the store.""" - if id is None: - return - - for request in self._stored.requests: - if request.get("id") == id: - if tag: - request["tag"] = tag - if ops: - request["ops"] = ops - request["state"] = state - return - - # New request - self._stored.requests.append( - {"id": id, "tag": tag, "ops": ops, "state": state} - ) - - def get_request_from_store(self, id: str) -> dict: - """Get request from the stote.""" - for request in self._stored.requests: - if request.get("id") == id: - return request - - return {} - - def is_request_processed(self, id: str) -> bool: - """Check if request is processed.""" - for request in self._stored.requests: - if ( - request.get("id") == id - and request.get("state") == REQUEST_PROCESSED - ): - return True - - return False - - def get_remote_app_data(self, key: str) -> Optional[str]: - """Return the value for the given key from remote app data.""" - if self._identity_resource_rel: - data = self._identity_resource_rel.data[ - self._identity_resource_rel.app - ] - return data.get(key) - - return None - - def ready(self) -> bool: - """Interface is ready or not. - - Interface is considered ready if the op request is processed - and response is sent. In case of non leader unit, just consider - the interface is ready. - """ - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, set the interface to ready") - return True - - try: - app_data = self._identity_resource_rel.data[self.charm.app] - if "request" not in app_data: - return False - - request = json.loads(app_data["request"]) - request_id = request.get("id") - response_id = self.response.get("id") - if request_id == response_id: - return True - except Exception as e: - logger.debug(str(e)) - - return False - - def request_ops(self, request: dict) -> None: - """Request keystone ops.""" - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, not sending request") - return - - id_ = request.get("id") - tag = request.get("tag") - ops = request.get("ops") - req = self.get_request_from_store(id_) - if req and req.get("state") == REQUEST_PROCESSED: - logger.debug("Request {id_} already processed") - return - - if not self._stored.provider_ready: - self.save_request_in_store(id_, tag, ops, REQUEST_NOT_SENT) - logger.debug("Keystone not yet ready to take requests") - return - - logger.debug("Requesting ops to keystone") - app_data = self._identity_resource_rel.data[self.charm.app] - app_data["request"] = json.dumps(request) - self.save_request_in_store(id_, tag, ops, REQUEST_SENT) - - -class IdentityOpsRequestEvent(EventBase): - """Has IdentityOpsRequest Event.""" - - def __init__(self, handle, relation_id, relation_name, request): - """Initialise event.""" - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.request = request - - def snapshot(self): - """Snapshot the event.""" - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "request": self.request, - } - - def restore(self, snapshot): - """Restore the event.""" - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.request = snapshot["request"] - - -class IdentityResourceProviderEvents(ObjectEvents): - """Events class for `on`.""" - - process_op = EventSource(IdentityOpsRequestEvent) - - -class IdentityResourceProvides(Object): - """IdentityResourceProvides class.""" - - on = IdentityResourceProviderEvents() - - def __init__(self, charm: CharmBase, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_resource_relation_changed, - ) - - def _on_identity_resource_relation_changed( - self, event: RelationChangedEvent - ): - """Handle IdentityResource changed.""" - request = event.relation.data[event.relation.app].get("request", {}) - self.on.process_op.emit( - event.relation.id, event.relation.name, request - ) - - def set_ops_response( - self, relation_id: str, relation_name: str, ops_response: dict - ) -> None: - """Set response to ops request.""" - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, not sending response") - return - - logger.debug("Update response from keystone") - _identity_resource_rel = self.charm.model.get_relation( - relation_name, relation_id - ) - if not _identity_resource_rel: - # Relation has disappeared so skip send of data - return - - app_data = _identity_resource_rel.data[self.charm.app] - app_data["response"] = json.dumps(ops_response) diff --git a/charms/barbican-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/barbican-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3f..00000000 --- a/charms/barbican-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -Also provide additional parameters to the charm object: - - service - - internal_url - - public_url - - admin_url - - region - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - service = "my-service" - internal_url = "http://internal-url" - public_url = "http://public-url" - admin_url = "http://admin-url" - region = "region" - ) - self.framework.observe( - self.identity_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# 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 - - -logger = logging.getLogger(__name__) - - -class IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._identity_service_rel.data[self._identity_service_rel.app] - return data.get(key) - - @property - def api_version(self) -> str: - """Return the api_version.""" - return self.get_remote_app_data('api-version') - - @property - def auth_host(self) -> str: - """Return the auth_host.""" - return self.get_remote_app_data('auth-host') - - @property - def auth_port(self) -> str: - """Return the auth_port.""" - return self.get_remote_app_data('auth-port') - - @property - def auth_protocol(self) -> str: - """Return the auth_protocol.""" - return self.get_remote_app_data('auth-protocol') - - @property - def internal_host(self) -> str: - """Return the internal_host.""" - return self.get_remote_app_data('internal-host') - - @property - def internal_port(self) -> str: - """Return the internal_port.""" - return self.get_remote_app_data('internal-port') - - @property - def internal_protocol(self) -> str: - """Return the internal_protocol.""" - return self.get_remote_app_data('internal-protocol') - - @property - def admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("password") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("username") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_credentials(self, relation_name: int, - relation_id: str, - api_version: str, - auth_host: str, - auth_port: str, - auth_protocol: str, - internal_host: str, - internal_port: str, - internal_protocol: str, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_rel.data[self.charm.app] - app_data["api-version"] = api_version - app_data["auth-host"] = auth_host - app_data["auth-port"] = str(auth_port) - app_data["auth-protocol"] = auth_protocol - app_data["internal-host"] = internal_host - app_data["internal-port"] = str(internal_port) - app_data["internal-protocol"] = internal_protocol - app_data["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/charms/barbican-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/barbican-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/barbican-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/barbican-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/barbican-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/barbican-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/barbican-k8s/osci.yaml b/charms/barbican-k8s/osci.yaml deleted file mode 100644 index 7e1239dd..00000000 --- a/charms/barbican-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: barbican-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/barbican-k8s/pyproject.toml b/charms/barbican-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/barbican-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/barbican-k8s/rename.sh b/charms/barbican-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/barbican-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/barbican-k8s/requirements.txt b/charms/barbican-k8s/requirements.txt index 0f23be49..b91c068e 100644 --- a/charms/barbican-k8s/requirements.txt +++ b/charms/barbican-k8s/requirements.txt @@ -13,4 +13,6 @@ lightkube-models ops pwgen pytest-interface-tester -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam + +# From ops_sunbeam +tenacity diff --git a/charms/barbican-k8s/src/templates/barbican.conf b/charms/barbican-k8s/src/templates/barbican.conf index 0253b41d..bd76ac38 100644 --- a/charms/barbican-k8s/src/templates/barbican.conf +++ b/charms/barbican-k8s/src/templates/barbican.conf @@ -10,6 +10,8 @@ sql_connection = {{ database.connection }} db_auto_create = false {% include "parts/section-identity" %} +# XXX Region should come from the id relation here +region_name = {{ options.region }} {% include "parts/section-service-user" %} diff --git a/charms/barbican-k8s/src/templates/parts/section-identity b/charms/barbican-k8s/src/templates/parts/section-identity deleted file mode 100644 index 92dabb09..00000000 --- a/charms/barbican-k8s/src/templates/parts/section-identity +++ /dev/null @@ -1,27 +0,0 @@ -[keystone_authtoken] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True - -# XXX Region should come from the id relation here -region_name = {{ options.region }} diff --git a/charms/barbican-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/barbican-k8s/src/templates/parts/section-oslo-messaging-rabbit deleted file mode 100644 index 145c4ee9..00000000 --- a/charms/barbican-k8s/src/templates/parts/section-oslo-messaging-rabbit +++ /dev/null @@ -1,2 +0,0 @@ -[oslo_messaging_rabbit] -rabbit_quorum_queue = True diff --git a/charms/barbican-k8s/test-requirements.txt b/charms/barbican-k8s/test-requirements.txt deleted file mode 100644 index 0b8ca0cd..00000000 --- a/charms/barbican-k8s/test-requirements.txt +++ /dev/null @@ -1,14 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -coverage -mock -flake8 -stestr -git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza -git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack -git+https://opendev.org/openstack/tempest.git#egg=tempest -ops diff --git a/charms/barbican-k8s/tests/unit/test_barbican_charm.py b/charms/barbican-k8s/tests/unit/test_barbican_charm.py index eff64acc..6dcaaa76 100644 --- a/charms/barbican-k8s/tests/unit/test_barbican_charm.py +++ b/charms/barbican-k8s/tests/unit/test_barbican_charm.py @@ -16,9 +16,8 @@ """Unit tests for Barbican operator.""" -import ops_sunbeam.test_utils as test_utils - import charm +import ops_sunbeam.test_utils as test_utils class _BarbicanTestOperatorCharm(charm.BarbicanOperatorCharm): diff --git a/charms/barbican-k8s/tox.ini b/charms/barbican-k8s/tox.ini deleted file mode 100644 index fbaa02c5..00000000 --- a/charms/barbican-k8s/tox.ini +++ /dev/null @@ -1,169 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - HOME - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/ceilometer-k8s/.gitignore b/charms/ceilometer-k8s/.gitignore deleted file mode 100644 index 24ff2e41..00000000 --- a/charms/ceilometer-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -*.charm -.tox/ -.coverage -__pycache__/ -*.py[cod] -.idea -.vscode/ -*.swp -.stestr/ diff --git a/charms/ceilometer-k8s/.gitreview b/charms/ceilometer-k8s/.gitreview deleted file mode 100644 index 1b4bcda6..00000000 --- a/charms/ceilometer-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-ceilometer-k8s.git -defaultbranch=main diff --git a/charms/ceilometer-k8s/.stestr.conf b/charms/ceilometer-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/ceilometer-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/ceilometer-k8s/.zuul.yaml b/charms/ceilometer-k8s/.zuul.yaml deleted file mode 100644 index 18a4323b..00000000 --- a/charms/ceilometer-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: ceilometer-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/ceilometer-k8s/charmcraft.yaml b/charms/ceilometer-k8s/charmcraft.yaml index ac49568b..98bf024f 100644 --- a/charms/ceilometer-k8s/charmcraft.yaml +++ b/charms/ceilometer-k8s/charmcraft.yaml @@ -27,4 +27,3 @@ parts: - cryptography - jsonschema - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/ceilometer-k8s/fetch-libs.sh b/charms/ceilometer-k8s/fetch-libs.sh deleted file mode 100755 index 16cf2cad..00000000 --- a/charms/ceilometer-k8s/fetch-libs.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.gnocchi_k8s.v0.gnocchi_service -# charmcraft fetch-lib charms.data_platform_libs.v0.database_requires -# charmcraft fetch-lib charms.keystone_k8s.v1.identity_service -# charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq -# charmcraft fetch-lib charms.traefik_k8s.v1.ingress diff --git a/charms/ceilometer-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/ceilometer-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/ceilometer-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/ceilometer-k8s/osci.yaml b/charms/ceilometer-k8s/osci.yaml deleted file mode 100644 index 05db36a3..00000000 --- a/charms/ceilometer-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: ceilometer-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/ceilometer-k8s/pyproject.toml b/charms/ceilometer-k8s/pyproject.toml deleted file mode 100644 index 30821404..00000000 --- a/charms/ceilometer-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/ceilometer-k8s/rename.sh b/charms/ceilometer-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/ceilometer-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/ceilometer-k8s/requirements.txt b/charms/ceilometer-k8s/requirements.txt index 20a477f7..9e7a45a8 100644 --- a/charms/ceilometer-k8s/requirements.txt +++ b/charms/ceilometer-k8s/requirements.txt @@ -1,8 +1,10 @@ ops jinja2 -git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam lightkube # Uncomment below if charm relates to ceph # git+https://github.com/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client # git+https://github.com/juju/charm-helpers.git#egg=charmhelpers + +# From ops_sunbeam +tenacity diff --git a/charms/ceilometer-k8s/src/templates/ceilometer.conf b/charms/ceilometer-k8s/src/templates/ceilometer.conf index f976d370..1a1b265e 100644 --- a/charms/ceilometer-k8s/src/templates/ceilometer.conf +++ b/charms/ceilometer-k8s/src/templates/ceilometer.conf @@ -20,8 +20,8 @@ archive_policy = low [keystone_authtoken] {% include "parts/identity-data-id-creds" %} -{% include "parts/section-service-user-id-creds" %} +{% include "parts/section-service-user-from-identity-credentials" %} -{% include "parts/section-service-credentials" %} +{% include "parts/section-service-credentials-from-identity-service" %} {% include "parts/section-oslo-messaging-rabbit" %} diff --git a/charms/ceilometer-k8s/src/templates/parts/database-connection b/charms/ceilometer-k8s/src/templates/parts/database-connection deleted file mode 100644 index 1fd70ce2..00000000 --- a/charms/ceilometer-k8s/src/templates/parts/database-connection +++ /dev/null @@ -1,3 +0,0 @@ -{% if database.connection -%} -connection = {{ database.connection }} -{% endif -%} diff --git a/charms/ceilometer-k8s/src/templates/parts/identity-data b/charms/ceilometer-k8s/src/templates/parts/identity-data deleted file mode 100644 index 706d9d13..00000000 --- a/charms/ceilometer-k8s/src/templates/parts/identity-data +++ /dev/null @@ -1,23 +0,0 @@ -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True diff --git a/charms/ceilometer-k8s/src/templates/parts/section-database b/charms/ceilometer-k8s/src/templates/parts/section-database deleted file mode 100644 index 986d9b10..00000000 --- a/charms/ceilometer-k8s/src/templates/parts/section-database +++ /dev/null @@ -1,3 +0,0 @@ -[database] -{% include "parts/database-connection" %} -connection_recycle_time = 200 diff --git a/charms/ceilometer-k8s/src/templates/parts/section-federation b/charms/ceilometer-k8s/src/templates/parts/section-federation deleted file mode 100644 index 65ee99ed..00000000 --- a/charms/ceilometer-k8s/src/templates/parts/section-federation +++ /dev/null @@ -1,10 +0,0 @@ -{% if trusted_dashboards %} -[federation] -{% for dashboard_url in trusted_dashboards -%} -trusted_dashboard = {{ dashboard_url }} -{% endfor -%} -{% endif %} -{% for sp in fid_sps -%} -[{{ sp['protocol-name'] }}] -remote_id_attribute = {{ sp['remote-id-attribute'] }} -{% endfor -%} diff --git a/charms/ceilometer-k8s/src/templates/parts/section-identity b/charms/ceilometer-k8s/src/templates/parts/section-identity deleted file mode 100644 index 7568a9a4..00000000 --- a/charms/ceilometer-k8s/src/templates/parts/section-identity +++ /dev/null @@ -1,2 +0,0 @@ -[keystone_authtoken] -{% include "parts/identity-data" %} diff --git a/charms/ceilometer-k8s/src/templates/parts/section-middleware b/charms/ceilometer-k8s/src/templates/parts/section-middleware deleted file mode 100644 index e65f1d98..00000000 --- a/charms/ceilometer-k8s/src/templates/parts/section-middleware +++ /dev/null @@ -1,6 +0,0 @@ -{% for section in sections -%} -[{{section}}] -{% for key, value in sections[section].items() -%} -{{ key }} = {{ value }} -{% endfor %} -{%- endfor %} diff --git a/charms/ceilometer-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/ceilometer-k8s/src/templates/parts/section-oslo-messaging-rabbit deleted file mode 100644 index 145c4ee9..00000000 --- a/charms/ceilometer-k8s/src/templates/parts/section-oslo-messaging-rabbit +++ /dev/null @@ -1,2 +0,0 @@ -[oslo_messaging_rabbit] -rabbit_quorum_queue = True diff --git a/charms/ceilometer-k8s/src/templates/parts/section-service-user b/charms/ceilometer-k8s/src/templates/parts/section-service-user deleted file mode 100644 index 165fbe71..00000000 --- a/charms/ceilometer-k8s/src/templates/parts/section-service-user +++ /dev/null @@ -1,15 +0,0 @@ -{% if identity_service.service_domain_id -%} -[service_user] -{% if identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -send_service_user_token = true -auth_type = password -project_domain_id = {{ identity_service.service_domain_id }} -user_domain_id = {{ identity_service.service_domain_id }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -{% endif -%} diff --git a/charms/ceilometer-k8s/src/templates/parts/section-signing b/charms/ceilometer-k8s/src/templates/parts/section-signing deleted file mode 100644 index cb7d69ae..00000000 --- a/charms/ceilometer-k8s/src/templates/parts/section-signing +++ /dev/null @@ -1,15 +0,0 @@ -{% if enable_signing -%} -[signing] -{% if certfile -%} -certfile = {{ certfile }} -{% endif -%} -{% if keyfile -%} -keyfile = {{ keyfile }} -{% endif -%} -{% if ca_certs -%} -ca_certs = {{ ca_certs }} -{% endif -%} -{% if ca_key -%} -ca_key = {{ ca_key }} -{% endif -%} -{% endif -%} \ No newline at end of file diff --git a/charms/ceilometer-k8s/test-requirements.txt b/charms/ceilometer-k8s/test-requirements.txt deleted file mode 100644 index 276e5bee..00000000 --- a/charms/ceilometer-k8s/test-requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -# This file is managed centrally. If you find the need to modify this as a -# one-off, please don't. Intead, consult #openstack-charms and ask about -# requirements management in charms via bot-control. Thank you. - -coverage -mock -flake8 -stestr -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/ceilometer-k8s/tests/unit/test_charm.py b/charms/ceilometer-k8s/tests/unit/test_charm.py index ff3e7847..4a287b2d 100644 --- a/charms/ceilometer-k8s/tests/unit/test_charm.py +++ b/charms/ceilometer-k8s/tests/unit/test_charm.py @@ -16,9 +16,8 @@ """Tests for gnocchi charm.""" -import ops_sunbeam.test_utils as test_utils - import charm +import ops_sunbeam.test_utils as test_utils class _CeilometerOperatorCharm(charm.CeilometerOperatorCharm): diff --git a/charms/ceilometer-k8s/tox.ini b/charms/ceilometer-k8s/tox.ini deleted file mode 100644 index 067963f9..00000000 --- a/charms/ceilometer-k8s/tox.ini +++ /dev/null @@ -1,165 +0,0 @@ -# Operator charm (with zaza): tox.ini - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - HOME - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/cinder-ceph-k8s/.flake8 b/charms/cinder-ceph-k8s/.flake8 deleted file mode 100644 index 8ef84fcd..00000000 --- a/charms/cinder-ceph-k8s/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 99 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/charms/cinder-ceph-k8s/.gitignore b/charms/cinder-ceph-k8s/.gitignore deleted file mode 100644 index ba40a601..00000000 --- a/charms/cinder-ceph-k8s/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -venv/ -build/ -*.charm - -.coverage -__pycache__/ -*.py[cod] -.tox -.stestr diff --git a/charms/cinder-ceph-k8s/.gitreview b/charms/cinder-ceph-k8s/.gitreview deleted file mode 100644 index 3b9d90fa..00000000 --- a/charms/cinder-ceph-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-cinder-ceph-k8s.git -defaultbranch=main diff --git a/charms/cinder-ceph-k8s/.jujuignore b/charms/cinder-ceph-k8s/.jujuignore deleted file mode 100644 index 56ce099b..00000000 --- a/charms/cinder-ceph-k8s/.jujuignore +++ /dev/null @@ -1,5 +0,0 @@ -/venv -*.py[cod] -*.charm -.tox -.stestr diff --git a/charms/cinder-ceph-k8s/.stestr.conf b/charms/cinder-ceph-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/cinder-ceph-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/cinder-ceph-k8s/.zuul.yaml b/charms/cinder-ceph-k8s/.zuul.yaml deleted file mode 100644 index 3176ef44..00000000 --- a/charms/cinder-ceph-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: cinder-ceph-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/cinder-ceph-k8s/charmcraft.yaml b/charms/cinder-ceph-k8s/charmcraft.yaml index 0c6b3bdb..cbd210b0 100644 --- a/charms/cinder-ceph-k8s/charmcraft.yaml +++ b/charms/cinder-ceph-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/cinder-ceph-k8s/fetch-libs.sh b/charms/cinder-ceph-k8s/fetch-libs.sh deleted file mode 100755 index defa6987..00000000 --- a/charms/cinder-ceph-k8s/fetch-libs.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.data_platform_libs.v0.database_requires -charmcraft fetch-lib charms.keystone_k8s.v1.identity_service -charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.cinder_k8s.v0.storage_backend -charmcraft fetch-lib charms.traefik_k8s.v2.ingress diff --git a/charms/cinder-ceph-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/cinder-ceph-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 35556622..00000000 --- a/charms/cinder-ceph-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,518 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -Also provide additional parameters to the charm object: - - service - - internal_url - - public_url - - admin_url - - region - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - service = "my-service" - internal_url = "http://internal-url" - public_url = "http://public-url" - admin_url = "http://admin-url" - region = "region" - ) - self.framework.observe( - self.identity_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# 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 = 0 - - -logger = logging.getLogger(__name__) - - -class IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._identity_service_rel.data[self._identity_service_rel.app] - return data.get(key) - - @property - def api_version(self) -> str: - """Return the api_version.""" - return self.get_remote_app_data('api-version') - - @property - def auth_host(self) -> str: - """Return the auth_host.""" - return self.get_remote_app_data('auth-host') - - @property - def auth_port(self) -> str: - """Return the auth_port.""" - return self.get_remote_app_data('auth-port') - - @property - def auth_protocol(self) -> str: - """Return the auth_protocol.""" - return self.get_remote_app_data('auth-protocol') - - @property - def internal_host(self) -> str: - """Return the internal_host.""" - return self.get_remote_app_data('internal-host') - - @property - def internal_port(self) -> str: - """Return the internal_port.""" - return self.get_remote_app_data('internal-port') - - @property - def internal_protocol(self) -> str: - """Return the internal_protocol.""" - return self.get_remote_app_data('internal-protocol') - - @property - def admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("password") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("username") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_credentials(self, relation_name: int, - relation_id: str, - api_version: str, - auth_host: str, - auth_port: str, - auth_protocol: str, - internal_host: str, - internal_port: str, - internal_protocol: str, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_rel.data[self.charm.app] - app_data["api-version"] = api_version - app_data["auth-host"] = auth_host - app_data["auth-port"] = str(auth_port) - app_data["auth-protocol"] = auth_protocol - app_data["internal-host"] = internal_host - app_data["internal-port"] = str(internal_port) - app_data["internal-protocol"] = internal_protocol - app_data["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials diff --git a/charms/cinder-ceph-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/cinder-ceph-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/cinder-ceph-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/cinder-ceph-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/cinder-ceph-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/cinder-ceph-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/cinder-ceph-k8s/osci.yaml b/charms/cinder-ceph-k8s/osci.yaml deleted file mode 100644 index 2d77a6de..00000000 --- a/charms/cinder-ceph-k8s/osci.yaml +++ /dev/null @@ -1,12 +0,0 @@ -- project: - templates: - - charm-unit-jobs-py38 - - charm-unit-jobs-py310 - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: cinder-ceph-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/cinder-ceph-k8s/pyproject.toml b/charms/cinder-ceph-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/cinder-ceph-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/cinder-ceph-k8s/rename.sh b/charms/cinder-ceph-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/cinder-ceph-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/cinder-ceph-k8s/requirements.txt b/charms/cinder-ceph-k8s/requirements.txt index e3e15ef9..6a1de2d9 100644 --- a/charms/cinder-ceph-k8s/requirements.txt +++ b/charms/cinder-ceph-k8s/requirements.txt @@ -11,7 +11,6 @@ lightkube lightkube-models requests # Drop - not needed in storage backend interface. ops -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam git+https://opendev.org/openstack/charm-ops-interface-tls-certificates#egg=interface_tls_certificates @@ -23,3 +22,6 @@ git+https://github.com/juju/charm-helpers.git#egg=charmhelpers # TODO requests # Drop - not needed in storage backend interface. netifaces # Drop when charmhelpers dependency is removed. + +# From ops_sunbeam +tenacity diff --git a/charms/cinder-ceph-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/cinder-ceph-k8s/src/templates/parts/section-oslo-messaging-rabbit deleted file mode 100644 index 145c4ee9..00000000 --- a/charms/cinder-ceph-k8s/src/templates/parts/section-oslo-messaging-rabbit +++ /dev/null @@ -1,2 +0,0 @@ -[oslo_messaging_rabbit] -rabbit_quorum_queue = True diff --git a/charms/cinder-ceph-k8s/test-requirements.txt b/charms/cinder-ceph-k8s/test-requirements.txt deleted file mode 100644 index a9b0d698..00000000 --- a/charms/cinder-ceph-k8s/test-requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -coverage -mock -flake8 -stestr -git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza -git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack -git+https://opendev.org/openstack/tempest.git#egg=tempest -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/cinder-ceph-k8s/tests/unit/test_cinder_ceph_charm.py b/charms/cinder-ceph-k8s/tests/unit/test_cinder_ceph_charm.py index 0fd046cd..7eaad75c 100644 --- a/charms/cinder-ceph-k8s/tests/unit/test_cinder_ceph_charm.py +++ b/charms/cinder-ceph-k8s/tests/unit/test_cinder_ceph_charm.py @@ -18,16 +18,16 @@ import json +import charm import ops_sunbeam.test_utils as test_utils from mock import ( + MagicMock, patch, ) from ops.testing import ( Harness, ) -import charm - class _CinderCephOperatorCharm(charm.CinderCephOperatorCharm): """Charm wrapper for test usage.""" @@ -82,6 +82,7 @@ class TestCinderCephOperatorCharm(test_utils.CharmTestCase): def setUp(self): """Setup fixtures ready for testing.""" super().setUp(charm, self.PATCHES) + self.mock_event = MagicMock() self.harness = test_utils.get_harness( _CinderCephOperatorCharm, container_calls=self.container_calls ) @@ -118,7 +119,13 @@ class TestCinderCephOperatorCharm(test_utils.CharmTestCase): test_utils.add_complete_db_relation(self.harness) add_complete_storage_backend_relation(self.harness) test_utils.set_all_pebbles_ready(self.harness) - self.assertTrue(self.harness.charm.relation_handlers_ready()) + self.assertSetEqual( + self.harness.charm.get_mandatory_relations_not_ready( + self.mock_event + ), + set(), + ) + # self.assertTrue(self.harness.charm.relation_handlers_ready()) def test_ceph_access(self): """Test charm provides secret via ceph-access.""" @@ -132,7 +139,13 @@ class TestCinderCephOperatorCharm(test_utils.CharmTestCase): ) add_complete_storage_backend_relation(self.harness) test_utils.set_all_pebbles_ready(self.harness) - self.assertTrue(self.harness.charm.relation_handlers_ready()) + # self.assertTrue(self.harness.charm.relation_handlers_ready()) + self.assertSetEqual( + self.harness.charm.get_mandatory_relations_not_ready( + self.mock_event + ), + set(), + ) rel_data = self.harness.get_relation_data( access_rel, self.harness.charm.unit.app.name ) diff --git a/charms/cinder-ceph-k8s/tox.ini b/charms/cinder-ceph-k8s/tox.ini deleted file mode 100644 index a411adb3..00000000 --- a/charms/cinder-ceph-k8s/tox.ini +++ /dev/null @@ -1,160 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/cinder-k8s/.flake8 b/charms/cinder-k8s/.flake8 deleted file mode 100644 index 8ef84fcd..00000000 --- a/charms/cinder-k8s/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 99 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/charms/cinder-k8s/.gitignore b/charms/cinder-k8s/.gitignore deleted file mode 100644 index 844a0b93..00000000 --- a/charms/cinder-k8s/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -venv/ -build/ -.stestr/ -*.charm -.tox -.coverage -__pycache__/ -*.py[cod] diff --git a/charms/cinder-k8s/.gitreview b/charms/cinder-k8s/.gitreview deleted file mode 100644 index e2834fe8..00000000 --- a/charms/cinder-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-cinder-k8s.git -defaultbranch=main diff --git a/charms/cinder-k8s/.jujuignore b/charms/cinder-k8s/.jujuignore deleted file mode 100644 index 6ccd559e..00000000 --- a/charms/cinder-k8s/.jujuignore +++ /dev/null @@ -1,3 +0,0 @@ -/venv -*.py[cod] -*.charm diff --git a/charms/cinder-k8s/.stestr.conf b/charms/cinder-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/cinder-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/cinder-k8s/.zuul.yaml b/charms/cinder-k8s/.zuul.yaml deleted file mode 100644 index 8fdc72b1..00000000 --- a/charms/cinder-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: cinder-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/cinder-k8s/charmcraft.yaml b/charms/cinder-k8s/charmcraft.yaml index 2fdc318d..9556149e 100644 --- a/charms/cinder-k8s/charmcraft.yaml +++ b/charms/cinder-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/cinder-k8s/fetch-libs.sh b/charms/cinder-k8s/fetch-libs.sh deleted file mode 100755 index e7772471..00000000 --- a/charms/cinder-k8s/fetch-libs.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.data_platform_libs.v0.database_requires -charmcraft fetch-lib charms.keystone_k8s.v1.identity_service -charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.traefik_k8s.v2.ingress diff --git a/charms/cinder-k8s/lib/charms/cinder_k8s/v0/storage_backend.py b/charms/cinder-k8s/lib/charms/cinder_k8s/v0/storage_backend.py deleted file mode 100644 index 8b1aa804..00000000 --- a/charms/cinder-k8s/lib/charms/cinder_k8s/v0/storage_backend.py +++ /dev/null @@ -1,189 +0,0 @@ -"""TODO: Add a proper docstring here. - -This is a placeholder docstring for this charm library. Docstrings are -presented on Charmhub and updated whenever you push a new version of the -library. - -Complete documentation about creating and documenting libraries can be found -in the SDK docs at https://juju.is/docs/sdk/libraries. - -See `charmcraft publish-lib` and `charmcraft fetch-lib` for details of how to -share and consume charm libraries. They serve to enhance collaboration -between charmers. Use a charmer's libraries for classes that handle -integration with their charm. - -Bear in mind that new revisions of the different major API versions (v0, v1, -v2 etc) are maintained independently. You can continue to update v0 and v1 -after you have pushed v3. - -Markdown is supported, following the CommonMark specification. -""" - -# The unique Charmhub library identifier, never change it -LIBID = "68536ea2f06d40078ccbedd7095e141c" - -# 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 - -import json -import logging -import requests - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -# TODO: add your code here! Happy coding! -class StorageBackendConnectedEvent(EventBase): - """StorageBackend connected Event.""" - - pass - - -class StorageBackendReadyEvent(EventBase): - """StorageBackend ready for use Event.""" - - pass - - -class StorageBackendGoneAwayEvent(EventBase): - """StorageBackend relation has gone-away Event""" - - pass - - -class StorageBackendServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(StorageBackendConnectedEvent) - ready = EventSource(StorageBackendReadyEvent) - goneaway = EventSource(StorageBackendGoneAwayEvent) - - -class StorageBackendRequires(Object): - """ - StorageBackendRequires class - """ - - on = StorageBackendServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_storage_backend_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_storage_backend_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_storage_backend_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_storage_backend_relation_broken, - ) - - def _on_storage_backend_relation_joined(self, event): - """StorageBackend relation joined.""" - logging.debug("StorageBackendRequires on_joined") - self.on.connected.emit() - - def _on_storage_backend_relation_changed(self, event): - """StorageBackend relation changed.""" - logging.debug("StorageBackendRequires on_changed") - self.on.ready.emit() - - def _on_storage_backend_relation_broken(self, event): - """StorageBackend relation broken.""" - logging.debug("StorageBackendRequires on_broken") - self.on.goneaway.emit() - - def set_ready(self) -> None: - """Request access to the StorageBackend server.""" - if self.model.unit.is_leader(): - logging.debug( - "Signalling storage backends that core services are ready" - ) - for relation in self.framework.model.relations[self.relation_name]: - relation.data[self.charm.app]["ready"] = 'true' - - -class APIReadyEvent(EventBase): - """StorageBackendClients Ready Event.""" - - pass - - -class StorageBackendClientEvents(ObjectEvents): - """Events class for `on`""" - - api_ready = EventSource(APIReadyEvent) - - -class StorageBackendProvides(Object): - """ - StorageBackendProvides class - """ - - on = StorageBackendClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_storage_backend_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_storage_backend_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_storage_backend_relation_broken, - ) - - def _on_storage_backend_relation_joined(self, event): - """Handle StorageBackend joined.""" - logging.debug("StorageBackendProvides on_joined") - - def remote_ready(self): - relation = self.framework.model.get_relation(self.relation_name) - if relation: - ready = relation.data[relation.app].get("ready") - return ready and json.loads(ready) - return False - - def _on_storage_backend_relation_changed(self, event): - """Handle StorageBackend changed.""" - logging.debug("StorageBackendProvides on_changed") - if self.remote_ready(): - self.on.api_ready.emit() - - def _on_storage_backend_relation_broken(self, event): - """Handle StorageBackend broken.""" - logging.debug("RabbitMQStorageBackendProvides on_departed") - # TODO clear data on the relation diff --git a/charms/cinder-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/cinder-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 53d61912..00000000 --- a/charms/cinder-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,496 +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. - -"""Relation 'requires' side abstraction for database relation. - -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.database_requires import 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.database_requires import 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, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(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 password(self) -> Optional[str]: - """Returns the password for the created user.""" - return self.relation.data[self.relation.app].get("password") - - @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 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") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - return self.relation.data[self.relation.app].get("username") - - @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(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(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) - - -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 — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() - - 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) - self.charm = charm - self.database = database_name - 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.relations_aliases = relations_aliases - 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 - ) - - # 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 _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. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].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] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - 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 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) - - 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") - - @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]) diff --git a/charms/cinder-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/cinder-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3f..00000000 --- a/charms/cinder-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -Also provide additional parameters to the charm object: - - service - - internal_url - - public_url - - admin_url - - region - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - service = "my-service" - internal_url = "http://internal-url" - public_url = "http://public-url" - admin_url = "http://admin-url" - region = "region" - ) - self.framework.observe( - self.identity_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# 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 - - -logger = logging.getLogger(__name__) - - -class IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._identity_service_rel.data[self._identity_service_rel.app] - return data.get(key) - - @property - def api_version(self) -> str: - """Return the api_version.""" - return self.get_remote_app_data('api-version') - - @property - def auth_host(self) -> str: - """Return the auth_host.""" - return self.get_remote_app_data('auth-host') - - @property - def auth_port(self) -> str: - """Return the auth_port.""" - return self.get_remote_app_data('auth-port') - - @property - def auth_protocol(self) -> str: - """Return the auth_protocol.""" - return self.get_remote_app_data('auth-protocol') - - @property - def internal_host(self) -> str: - """Return the internal_host.""" - return self.get_remote_app_data('internal-host') - - @property - def internal_port(self) -> str: - """Return the internal_port.""" - return self.get_remote_app_data('internal-port') - - @property - def internal_protocol(self) -> str: - """Return the internal_protocol.""" - return self.get_remote_app_data('internal-protocol') - - @property - def admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("password") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("username") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_credentials(self, relation_name: int, - relation_id: str, - api_version: str, - auth_host: str, - auth_port: str, - auth_protocol: str, - internal_host: str, - internal_port: str, - internal_protocol: str, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_rel.data[self.charm.app] - app_data["api-version"] = api_version - app_data["auth-host"] = auth_host - app_data["auth-port"] = str(auth_port) - app_data["auth-protocol"] = auth_protocol - app_data["internal-host"] = internal_host - app_data["internal-port"] = str(internal_port) - app_data["internal-protocol"] = internal_protocol - app_data["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/charms/cinder-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py b/charms/cinder-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py deleted file mode 100644 index c8d2e0b1..00000000 --- a/charms/cinder-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py +++ /dev/null @@ -1,211 +0,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 - - 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 = 9 - -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", - "retry-errors", - "rewrite-target", - "rewrite-enabled", - "service-namespace", - "session-cookie-max-age", - "tls-secret-name", - "path-routes", -} - - -class IngressAvailableEvent(EventBase): - pass - - -class IngressCharmEvents(CharmEvents): - """Custom charm events.""" - - ingress_available = EventSource(IngressAvailableEvent) - - -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(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.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() diff --git a/charms/cinder-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/cinder-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/cinder-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/cinder-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/cinder-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/cinder-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/cinder-k8s/osci.yaml b/charms/cinder-k8s/osci.yaml deleted file mode 100644 index 13dbaa2d..00000000 --- a/charms/cinder-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: cinder-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/cinder-k8s/pyproject.toml b/charms/cinder-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/cinder-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/cinder-k8s/rename.sh b/charms/cinder-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/cinder-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/cinder-k8s/requirements.txt b/charms/cinder-k8s/requirements.txt index c92b43df..7743a6fe 100644 --- a/charms/cinder-k8s/requirements.txt +++ b/charms/cinder-k8s/requirements.txt @@ -11,9 +11,11 @@ pydantic<2.0 lightkube lightkube-models ops -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam git+https://opendev.org/openstack/charm-ops-interface-tls-certificates#egg=interface_tls_certificates # TODO requests # Drop - not needed in storage backend interface. + +# From ops_sunbeam +tenacity diff --git a/charms/cinder-k8s/src/templates/parts/section-database b/charms/cinder-k8s/src/templates/parts/section-database deleted file mode 100644 index eb52f65e..00000000 --- a/charms/cinder-k8s/src/templates/parts/section-database +++ /dev/null @@ -1,7 +0,0 @@ -[database] -{% if database.connection -%} -connection = {{ database.connection }} -{% else -%} -connection = sqlite:////var/lib/cinder/cinder.db -{% endif -%} -connection_recycle_time = 200 diff --git a/charms/cinder-k8s/src/templates/parts/section-federation b/charms/cinder-k8s/src/templates/parts/section-federation deleted file mode 100644 index 65ee99ed..00000000 --- a/charms/cinder-k8s/src/templates/parts/section-federation +++ /dev/null @@ -1,10 +0,0 @@ -{% if trusted_dashboards %} -[federation] -{% for dashboard_url in trusted_dashboards -%} -trusted_dashboard = {{ dashboard_url }} -{% endfor -%} -{% endif %} -{% for sp in fid_sps -%} -[{{ sp['protocol-name'] }}] -remote_id_attribute = {{ sp['remote-id-attribute'] }} -{% endfor -%} diff --git a/charms/cinder-k8s/src/templates/parts/section-identity b/charms/cinder-k8s/src/templates/parts/section-identity deleted file mode 100644 index cbb1d069..00000000 --- a/charms/cinder-k8s/src/templates/parts/section-identity +++ /dev/null @@ -1,24 +0,0 @@ -[keystone_authtoken] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True diff --git a/charms/cinder-k8s/src/templates/parts/section-middleware b/charms/cinder-k8s/src/templates/parts/section-middleware deleted file mode 100644 index e65f1d98..00000000 --- a/charms/cinder-k8s/src/templates/parts/section-middleware +++ /dev/null @@ -1,6 +0,0 @@ -{% for section in sections -%} -[{{section}}] -{% for key, value in sections[section].items() -%} -{{ key }} = {{ value }} -{% endfor %} -{%- endfor %} diff --git a/charms/cinder-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/cinder-k8s/src/templates/parts/section-oslo-messaging-rabbit deleted file mode 100644 index 145c4ee9..00000000 --- a/charms/cinder-k8s/src/templates/parts/section-oslo-messaging-rabbit +++ /dev/null @@ -1,2 +0,0 @@ -[oslo_messaging_rabbit] -rabbit_quorum_queue = True diff --git a/charms/cinder-k8s/src/templates/parts/section-service-user b/charms/cinder-k8s/src/templates/parts/section-service-user deleted file mode 100644 index 65103693..00000000 --- a/charms/cinder-k8s/src/templates/parts/section-service-user +++ /dev/null @@ -1,17 +0,0 @@ -{% if identity_service.service_domain_id -%} -[service_user] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -send_service_user_token = true -auth_type = password -project_domain_id = {{ identity_service.service_domain_id }} -user_domain_id = {{ identity_service.service_domain_id }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -{% endif -%} diff --git a/charms/cinder-k8s/src/templates/parts/section-signing b/charms/cinder-k8s/src/templates/parts/section-signing deleted file mode 100644 index cb7d69ae..00000000 --- a/charms/cinder-k8s/src/templates/parts/section-signing +++ /dev/null @@ -1,15 +0,0 @@ -{% if enable_signing -%} -[signing] -{% if certfile -%} -certfile = {{ certfile }} -{% endif -%} -{% if keyfile -%} -keyfile = {{ keyfile }} -{% endif -%} -{% if ca_certs -%} -ca_certs = {{ ca_certs }} -{% endif -%} -{% if ca_key -%} -ca_key = {{ ca_key }} -{% endif -%} -{% endif -%} \ No newline at end of file diff --git a/charms/cinder-k8s/test-requirements.txt b/charms/cinder-k8s/test-requirements.txt deleted file mode 100644 index a9b0d698..00000000 --- a/charms/cinder-k8s/test-requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -coverage -mock -flake8 -stestr -git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza -git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack -git+https://opendev.org/openstack/tempest.git#egg=tempest -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/cinder-k8s/tests/unit/test_cinder_charm.py b/charms/cinder-k8s/tests/unit/test_cinder_charm.py index 712e21d9..4ff7d581 100644 --- a/charms/cinder-k8s/tests/unit/test_cinder_charm.py +++ b/charms/cinder-k8s/tests/unit/test_cinder_charm.py @@ -16,9 +16,8 @@ """Unit tests for core Cinder charm class.""" -import ops_sunbeam.test_utils as test_utils - import charm +import ops_sunbeam.test_utils as test_utils class _CinderOperatorCharm(charm.CinderOperatorCharm): diff --git a/charms/cinder-k8s/tox.ini b/charms/cinder-k8s/tox.ini deleted file mode 100644 index a411adb3..00000000 --- a/charms/cinder-k8s/tox.ini +++ /dev/null @@ -1,160 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/designate-bind-k8s/.gitignore b/charms/designate-bind-k8s/.gitignore deleted file mode 100644 index 24ff2e41..00000000 --- a/charms/designate-bind-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -*.charm -.tox/ -.coverage -__pycache__/ -*.py[cod] -.idea -.vscode/ -*.swp -.stestr/ diff --git a/charms/designate-bind-k8s/.gitreview b/charms/designate-bind-k8s/.gitreview deleted file mode 100644 index 15594b28..00000000 --- a/charms/designate-bind-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-designate-bind-k8s.git -defaultbranch=main diff --git a/charms/designate-bind-k8s/.stestr.conf b/charms/designate-bind-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/designate-bind-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/designate-bind-k8s/.zuul.yaml b/charms/designate-bind-k8s/.zuul.yaml deleted file mode 100644 index ef9a2ff4..00000000 --- a/charms/designate-bind-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: designate-bind-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/designate-bind-k8s/charmcraft.yaml b/charms/designate-bind-k8s/charmcraft.yaml index 9c5a769f..e24ab1f5 100644 --- a/charms/designate-bind-k8s/charmcraft.yaml +++ b/charms/designate-bind-k8s/charmcraft.yaml @@ -27,4 +27,3 @@ parts: - cryptography - jsonschema - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/designate-bind-k8s/fetch-libs.sh b/charms/designate-bind-k8s/fetch-libs.sh deleted file mode 100755 index d13dfc5c..00000000 --- a/charms/designate-bind-k8s/fetch-libs.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.observability-libs.v1.kubernetes_service_patch diff --git a/charms/designate-bind-k8s/osci.yaml b/charms/designate-bind-k8s/osci.yaml deleted file mode 100644 index 4cc088c3..00000000 --- a/charms/designate-bind-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: designate-bind-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 9/edge diff --git a/charms/designate-bind-k8s/pyproject.toml b/charms/designate-bind-k8s/pyproject.toml deleted file mode 100644 index 30821404..00000000 --- a/charms/designate-bind-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/designate-bind-k8s/rename.sh b/charms/designate-bind-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/designate-bind-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/designate-bind-k8s/requirements.txt b/charms/designate-bind-k8s/requirements.txt index cb87fb03..ccdfbaf8 100644 --- a/charms/designate-bind-k8s/requirements.txt +++ b/charms/designate-bind-k8s/requirements.txt @@ -1,5 +1,7 @@ ops jinja2 -git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam lightkube lightkube-models + +# From ops_sunbeam +tenacity diff --git a/charms/designate-bind-k8s/test-requirements.txt b/charms/designate-bind-k8s/test-requirements.txt deleted file mode 100644 index d1a61d34..00000000 --- a/charms/designate-bind-k8s/test-requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -# This file is managed centrally. If you find the need to modify this as a -# one-off, please don't. Intead, consult #openstack-charms and ask about -# requirements management in charms via bot-control. Thank you. - -coverage -mock -flake8 -stestr -ops diff --git a/charms/designate-bind-k8s/tests/unit/test_bind_charm.py b/charms/designate-bind-k8s/tests/unit/test_bind_charm.py index 90562b60..5bfb0074 100644 --- a/charms/designate-bind-k8s/tests/unit/test_bind_charm.py +++ b/charms/designate-bind-k8s/tests/unit/test_bind_charm.py @@ -14,9 +14,8 @@ """Unit tests.""" -import ops_sunbeam.test_utils as test_utils - import charm +import ops_sunbeam.test_utils as test_utils class _BindTestOperatorCharm(charm.BindOperatorCharm): diff --git a/charms/designate-bind-k8s/tox.ini b/charms/designate-bind-k8s/tox.ini deleted file mode 100644 index b14f9098..00000000 --- a/charms/designate-bind-k8s/tox.ini +++ /dev/null @@ -1,166 +0,0 @@ -# Operator charm (with zaza): tox.ini - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -project_lib_path = {toxinidir}/lib/charms/designate_bind_k8s -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} {[vars]project_lib_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - HOME - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/designate-k8s/.gitignore b/charms/designate-k8s/.gitignore deleted file mode 100644 index 24ff2e41..00000000 --- a/charms/designate-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -*.charm -.tox/ -.coverage -__pycache__/ -*.py[cod] -.idea -.vscode/ -*.swp -.stestr/ diff --git a/charms/designate-k8s/.gitreview b/charms/designate-k8s/.gitreview deleted file mode 100644 index 724badba..00000000 --- a/charms/designate-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-designate-k8s.git -defaultbranch=main diff --git a/charms/designate-k8s/.stestr.conf b/charms/designate-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/designate-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/designate-k8s/.zuul.yaml b/charms/designate-k8s/.zuul.yaml deleted file mode 100644 index 210ad0b5..00000000 --- a/charms/designate-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: designate-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/designate-k8s/charmcraft.yaml b/charms/designate-k8s/charmcraft.yaml index 2fdc318d..9556149e 100644 --- a/charms/designate-k8s/charmcraft.yaml +++ b/charms/designate-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/designate-k8s/fetch-libs.sh b/charms/designate-k8s/fetch-libs.sh deleted file mode 100755 index 5cdf501d..00000000 --- a/charms/designate-k8s/fetch-libs.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.data_platform_libs.v0.database_requires -charmcraft fetch-lib charms.keystone_k8s.v1.identity_service -charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.traefik_k8s.v2.ingress -charmcraft fetch-lib charms.designate_bind_k8s.v0.bind_rndc - diff --git a/charms/designate-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/designate-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 11ffd6ca..00000000 --- a/charms/designate-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,537 +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. - -r"""[DEPRECATED] Relation 'requires' side abstraction for database relation. - -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.database_requires 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.database_requires 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, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 = 6 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(RelationEvent): - """Base class for database events.""" - - @property - def endpoints(self) -> Optional[str]: - """Returns a comma separated list of read/write endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("endpoints") - - @property - def password(self) -> Optional[str]: - """Returns the password for the created user.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("password") - - @property - def read_only_endpoints(self) -> Optional[str]: - """Returns a comma separated list of read only endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("read-only-endpoints") - - @property - def replset(self) -> Optional[str]: - """Returns the replicaset name. - - MongoDB only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("replset") - - @property - def tls(self) -> Optional[str]: - """Returns whether TLS is configured.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls") - - @property - def tls_ca(self) -> Optional[str]: - """Returns TLS CA.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls-ca") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("username") - - @property - def version(self) -> Optional[str]: - """Returns the version of the database. - - Version as informed by the database daemon. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("version") - - -class DatabaseCreatedEvent(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(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) - - -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 — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() # pyright: ignore [reportGeneralTypeIssues] - - def __init__( - self, - charm, - relation_name: str, - database_name: str, - extra_user_roles: Optional[str] = None, - relations_aliases: Optional[List[str]] = None, - ): - """Manager of database client relations.""" - super().__init__(charm, relation_name) - self.charm = charm - self.database = database_name - 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.relations_aliases = relations_aliases - 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 - ) - - # 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 _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. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].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"} - if event.app - else {} - ) - - # 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] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - 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 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"} - if relation.app - else {} - ) - 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 _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()) - getattr(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()) - getattr(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()) - getattr(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") - - @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]) diff --git a/charms/designate-k8s/lib/charms/designate_bind_k8s/v0/bind_rndc.py b/charms/designate-k8s/lib/charms/designate_bind_k8s/v0/bind_rndc.py deleted file mode 100644 index 2bb2f93e..00000000 --- a/charms/designate-k8s/lib/charms/designate_bind_k8s/v0/bind_rndc.py +++ /dev/null @@ -1,364 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. -"""BindRndc Provides and Requires module. - -This library contains the Requires and Provides classes for handling -the bind_rndc interface. -Import `BindRndcRequires` in your charm, with the charm object and the -relation name: - - self - - "dns-backend" -Two events are also available to respond to: - - bind_rndc_ready - - goneaway -A basic example showing the usage of this relation follows: -``` -from charms.designate_bind_k8s.v0.bind_rndc import ( - BindRndcRequires -) -class BindRndcClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # BindRndc Requires - self.bind_rndc = BindRndcRequires( - self, "dns-backend" - ) - self.framework.observe( - self.bind_rndc.on.bind_rndc_ready, - self._on_bind_rndc_ready - ) - self.framework.observe( - self.bind_rndc.on.goneaway, - self._on_bind_rndc_goneaway - ) - def _on_bind_rndc_connected(self, event): - '''React to the Bind Rndc Connected event. - This event happens when BindRndc relation is added to the - model. - ''' - # Request the rndc key from the Bind Rndc relation. - self.bind_rndc.request_rndc_key("generated nonce") - def _on_bind_rndc_ready(self, event): - '''React to the Bind Rndc Ready event. - This event happens when BindRndc relation is added to the - model, relation is ready and/or relation data is changed. - ''' - # Do something with the configuration provided by relation. - pass - def _on_bind_rndc_goneaway(self, event): - '''React to the BindRndc goneaway event. - This event happens when BindRndc relation is removed. - ''' - # BindRndc Relation has goneaway. - pass -``` -""" - -import json -import logging -from typing import ( - Any, - Dict, - List, - Optional, - Union, -) - -import ops - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "1cb766c981874e7383d17cf54148b3d4" - -# 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 - - -class BindRndcConnectedEvent(ops.EventBase): - """Bind rndc connected event.""" - - def __init__( - self, - handle: ops.Handle, - relation_id: int, - relation_name: str, - ): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - - def snapshot(self) -> dict: - """Return snapshot data that should be persisted.""" - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - } - - def restore(self, snapshot: Dict[str, Any]): - """Restore the value state from a given snapshot.""" - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - - -class BindRndcReadyEvent(ops.EventBase): - """Bind rndc ready event.""" - - def __init__( - self, - handle: ops.Handle, - relation_id: int, - relation_name: str, - ): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - - def snapshot(self) -> dict: - """Return snapshot data that should be persisted.""" - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - } - - def restore(self, snapshot: Dict[str, Any]): - """Restore the value state from a given snapshot.""" - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - - -class BindRndcGoneAwayEvent(ops.EventBase): - """Bind rndc gone away event.""" - - pass - - -class BindRndcRequirerEvents(ops.ObjectEvents): - """List of events that the BindRndc requires charm can leverage.""" - - connected = ops.EventSource(BindRndcConnectedEvent) - ready = ops.EventSource(BindRndcReadyEvent) - goneaway = ops.EventSource(BindRndcGoneAwayEvent) - - -class BindRndcRequires(ops.Object): - """Class to be instantiated by the requiring side of the relation.""" - - on = BindRndcRequirerEvents() - - def __init__(self, charm: ops.CharmBase, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_relation_broken, - ) - - def _on_relation_joined(self, event: ops.RelationJoinedEvent): - """Handle relation joined event.""" - self.on.connected.emit( - event.relation.id, - event.relation.name, - ) - - def _on_relation_changed(self, event: ops.RelationJoinedEvent): - """Handle relation changed event.""" - host = self.host(event.relation) - rndc_key = self.get_rndc_key(event.relation) - - if all((host, rndc_key)): - self.on.ready.emit( - event.relation.id, - event.relation.name, - ) - - def _on_relation_broken(self, event: ops.RelationBrokenEvent): - """Handle relation broken event.""" - self.on.goneaway.emit() - - def host(self, relation: ops.Relation) -> Optional[str]: - """Return host from relation.""" - if relation.app is None: - return None - return relation.data[relation.app].get("host") - - def nonce(self, relation: ops.Relation) -> Optional[str]: - """Return nonce from relation.""" - return relation.data[self.charm.unit].get("nonce") - - def get_rndc_key(self, relation: ops.Relation) -> Optional[dict]: - """Get rndc keys.""" - if relation.app is None: - return None - if self.nonce(relation) is None: - logger.debug("No nonce set for unit yet") - return None - - return json.loads( - relation.data[relation.app].get("rndc_keys", "{}") - ).get(self.nonce(relation)) - - def request_rndc_key(self, relation: ops.Relation, nonce: str): - """Request rndc key over the relation.""" - relation.data[self.charm.unit]["nonce"] = nonce - - -class NewBindClientAttachedEvent(ops.EventBase): - """New bind client attached event.""" - - def __init__( - self, - handle: ops.Handle, - relation_id: int, - relation_name: str, - ): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - - def snapshot(self) -> dict: - """Return snapshot data that should be persisted.""" - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - } - - def restore(self, snapshot: Dict[str, Any]): - """Restore the value state from a given snapshot.""" - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - - -class BindClientUpdatedEvent(ops.EventBase): - """Bind client updated event.""" - - def __init__( - self, - handle: ops.Handle, - relation_id: int, - relation_name: str, - ): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - - def snapshot(self) -> dict: - """Return snapshot data that should be persisted.""" - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - } - - def restore(self, snapshot: Dict[str, Any]): - """Restore the value state from a given snapshot.""" - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - - -class BindRndcProviderEvents(ops.ObjectEvents): - """List of events that the BindRndc provider charm can leverage.""" - - new_bind_client_attached = ops.EventSource(NewBindClientAttachedEvent) - bind_client_updated = ops.EventSource(BindClientUpdatedEvent) - - -class BindRndcProvides(ops.Object): - """Class to be instantiated by the providing side of the relation.""" - - on = BindRndcProviderEvents() - - def __init__(self, charm: ops.CharmBase, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_relation_changed, - ) - - def _on_relation_joined(self, event: ops.RelationJoinedEvent): - self.on.new_bind_client_attached.emit( - event.relation.id, event.relation.name - ) - - def _on_relation_changed(self, event: ops.RelationChangedEvent): - self.on.bind_client_updated.emit( - event.relation.id, event.relation.name - ) - - def set_host(self, relation: ops.Relation, host: str): - """Set host on the relation.""" - if not self.charm.unit.is_leader(): - logger.debug("Not leader, skipping set_host") - return - relation.data[self.charm.app]["host"] = host - - def get_rndc_keys(self, relation: ops.Relation) -> dict: - """Get rndc keys.""" - return json.loads(relation.data[self.charm.app].get("rndc_keys", "{}")) - - def set_rndc_client_key( - self, - relation: ops.Relation, - client: str, - algorithm: str, - secret: ops.Secret, - ): - """Add rndc key to the relation. - - `rndc_keys` is a dict of dicts, keyed by client name. Each client - has an algorithm and secret property. The secret is a Juju secret id, - containing the actual secret needed to communicate over rndc. - """ - if not self.charm.unit.is_leader(): - logger.debug("Not leader, skipping set_rndc_client_key") - return - - keys = self.get_rndc_keys(relation) - keys[client] = { - "algorithm": algorithm, - "secret": secret.id, - } - - relation.data[self.charm.app]["rndc_keys"] = json.dumps( - keys, sort_keys=True - ) - - def remove_rndc_client_key( - self, - relation: ops.Relation, - client: Union[str, List[str]], - ): - """Remove rndc key from the relation.""" - if not self.charm.unit.is_leader(): - logger.debug("Not leader, skipping remove_rndc_client_key") - return - if isinstance(client, str): - client = [client] - keys = self.get_rndc_keys(relation) - for c in client: - keys.pop(c) - relation.data[self.charm.app]["rndc_keys"] = json.dumps( - keys, sort_keys=True - ) diff --git a/charms/designate-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/designate-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3f..00000000 --- a/charms/designate-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -Also provide additional parameters to the charm object: - - service - - internal_url - - public_url - - admin_url - - region - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - service = "my-service" - internal_url = "http://internal-url" - public_url = "http://public-url" - admin_url = "http://admin-url" - region = "region" - ) - self.framework.observe( - self.identity_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# 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 - - -logger = logging.getLogger(__name__) - - -class IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._identity_service_rel.data[self._identity_service_rel.app] - return data.get(key) - - @property - def api_version(self) -> str: - """Return the api_version.""" - return self.get_remote_app_data('api-version') - - @property - def auth_host(self) -> str: - """Return the auth_host.""" - return self.get_remote_app_data('auth-host') - - @property - def auth_port(self) -> str: - """Return the auth_port.""" - return self.get_remote_app_data('auth-port') - - @property - def auth_protocol(self) -> str: - """Return the auth_protocol.""" - return self.get_remote_app_data('auth-protocol') - - @property - def internal_host(self) -> str: - """Return the internal_host.""" - return self.get_remote_app_data('internal-host') - - @property - def internal_port(self) -> str: - """Return the internal_port.""" - return self.get_remote_app_data('internal-port') - - @property - def internal_protocol(self) -> str: - """Return the internal_protocol.""" - return self.get_remote_app_data('internal-protocol') - - @property - def admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("password") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("username") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_credentials(self, relation_name: int, - relation_id: str, - api_version: str, - auth_host: str, - auth_port: str, - auth_protocol: str, - internal_host: str, - internal_port: str, - internal_protocol: str, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_rel.data[self.charm.app] - app_data["api-version"] = api_version - app_data["auth-host"] = auth_host - app_data["auth-port"] = str(auth_port) - app_data["auth-protocol"] = auth_protocol - app_data["internal-host"] = internal_host - app_data["internal-port"] = str(internal_port) - app_data["internal-protocol"] = internal_protocol - app_data["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/charms/designate-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/designate-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/designate-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/designate-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/designate-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/designate-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/designate-k8s/osci.yaml b/charms/designate-k8s/osci.yaml deleted file mode 100644 index 9d3005c7..00000000 --- a/charms/designate-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: designate-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/designate-k8s/pyproject.toml b/charms/designate-k8s/pyproject.toml deleted file mode 100644 index 30821404..00000000 --- a/charms/designate-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/designate-k8s/rename.sh b/charms/designate-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/designate-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/designate-k8s/requirements.txt b/charms/designate-k8s/requirements.txt index db1e74a8..e0ef265f 100644 --- a/charms/designate-k8s/requirements.txt +++ b/charms/designate-k8s/requirements.txt @@ -2,5 +2,7 @@ ops jsonschema pydantic<2.0 jinja2 -git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam lightkube + +# From ops_sunbeam +tenacity diff --git a/charms/designate-k8s/src/templates/parts/database-connection b/charms/designate-k8s/src/templates/parts/database-connection deleted file mode 100644 index 1fd70ce2..00000000 --- a/charms/designate-k8s/src/templates/parts/database-connection +++ /dev/null @@ -1,3 +0,0 @@ -{% if database.connection -%} -connection = {{ database.connection }} -{% endif -%} diff --git a/charms/designate-k8s/src/templates/parts/identity-data b/charms/designate-k8s/src/templates/parts/identity-data deleted file mode 100644 index 706d9d13..00000000 --- a/charms/designate-k8s/src/templates/parts/identity-data +++ /dev/null @@ -1,23 +0,0 @@ -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True diff --git a/charms/designate-k8s/src/templates/parts/section-database b/charms/designate-k8s/src/templates/parts/section-database deleted file mode 100644 index b060b139..00000000 --- a/charms/designate-k8s/src/templates/parts/section-database +++ /dev/null @@ -1,4 +0,0 @@ -[database] -{% include "parts/database-connection" %} -connection_recycle_time = 200 -db_auto_create = false diff --git a/charms/designate-k8s/src/templates/parts/section-federation b/charms/designate-k8s/src/templates/parts/section-federation deleted file mode 100644 index 65ee99ed..00000000 --- a/charms/designate-k8s/src/templates/parts/section-federation +++ /dev/null @@ -1,10 +0,0 @@ -{% if trusted_dashboards %} -[federation] -{% for dashboard_url in trusted_dashboards -%} -trusted_dashboard = {{ dashboard_url }} -{% endfor -%} -{% endif %} -{% for sp in fid_sps -%} -[{{ sp['protocol-name'] }}] -remote_id_attribute = {{ sp['remote-id-attribute'] }} -{% endfor -%} diff --git a/charms/designate-k8s/src/templates/parts/section-identity b/charms/designate-k8s/src/templates/parts/section-identity deleted file mode 100644 index 7568a9a4..00000000 --- a/charms/designate-k8s/src/templates/parts/section-identity +++ /dev/null @@ -1,2 +0,0 @@ -[keystone_authtoken] -{% include "parts/identity-data" %} diff --git a/charms/designate-k8s/src/templates/parts/section-middleware b/charms/designate-k8s/src/templates/parts/section-middleware deleted file mode 100644 index e65f1d98..00000000 --- a/charms/designate-k8s/src/templates/parts/section-middleware +++ /dev/null @@ -1,6 +0,0 @@ -{% for section in sections -%} -[{{section}}] -{% for key, value in sections[section].items() -%} -{{ key }} = {{ value }} -{% endfor %} -{%- endfor %} diff --git a/charms/designate-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/designate-k8s/src/templates/parts/section-oslo-messaging-rabbit deleted file mode 100644 index 145c4ee9..00000000 --- a/charms/designate-k8s/src/templates/parts/section-oslo-messaging-rabbit +++ /dev/null @@ -1,2 +0,0 @@ -[oslo_messaging_rabbit] -rabbit_quorum_queue = True diff --git a/charms/designate-k8s/src/templates/parts/section-service-user b/charms/designate-k8s/src/templates/parts/section-service-user deleted file mode 100644 index 65103693..00000000 --- a/charms/designate-k8s/src/templates/parts/section-service-user +++ /dev/null @@ -1,17 +0,0 @@ -{% if identity_service.service_domain_id -%} -[service_user] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -send_service_user_token = true -auth_type = password -project_domain_id = {{ identity_service.service_domain_id }} -user_domain_id = {{ identity_service.service_domain_id }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -{% endif -%} diff --git a/charms/designate-k8s/src/templates/parts/section-signing b/charms/designate-k8s/src/templates/parts/section-signing deleted file mode 100644 index cb7d69ae..00000000 --- a/charms/designate-k8s/src/templates/parts/section-signing +++ /dev/null @@ -1,15 +0,0 @@ -{% if enable_signing -%} -[signing] -{% if certfile -%} -certfile = {{ certfile }} -{% endif -%} -{% if keyfile -%} -keyfile = {{ keyfile }} -{% endif -%} -{% if ca_certs -%} -ca_certs = {{ ca_certs }} -{% endif -%} -{% if ca_key -%} -ca_key = {{ ca_key }} -{% endif -%} -{% endif -%} \ No newline at end of file diff --git a/charms/designate-k8s/test-requirements.txt b/charms/designate-k8s/test-requirements.txt deleted file mode 100644 index f33eee5a..00000000 --- a/charms/designate-k8s/test-requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -# This file is managed centrally. If you find the need to modify this as a -# one-off, please don't. Intead, consult #openstack-charms and ask about -# requirements management in charms via bot-control. Thank you. - -coverage -mock -flake8 -stestr -ops -pytest-mock diff --git a/charms/designate-k8s/tests/unit/test_designate_charm.py b/charms/designate-k8s/tests/unit/test_designate_charm.py index 85a55ac3..05c2367d 100644 --- a/charms/designate-k8s/tests/unit/test_designate_charm.py +++ b/charms/designate-k8s/tests/unit/test_designate_charm.py @@ -18,13 +18,12 @@ import json +import charm import ops_sunbeam.test_utils as test_utils from ops.testing import ( Harness, ) -import charm - class _DesignateTestOperatorCharm(charm.DesignateOperatorCharm): """Test Operator Charm for Designate Operator.""" diff --git a/charms/designate-k8s/tox.ini b/charms/designate-k8s/tox.ini deleted file mode 100644 index 067963f9..00000000 --- a/charms/designate-k8s/tox.ini +++ /dev/null @@ -1,165 +0,0 @@ -# Operator charm (with zaza): tox.ini - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - HOME - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/glance-k8s/.flake8 b/charms/glance-k8s/.flake8 deleted file mode 100644 index 8ef84fcd..00000000 --- a/charms/glance-k8s/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 99 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/charms/glance-k8s/.gitignore b/charms/glance-k8s/.gitignore deleted file mode 100644 index 2d3a0ccd..00000000 --- a/charms/glance-k8s/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -venv/ -build/ -.stestr/ -*.charm -.tox -.coverage -__pycache__/ -*.py[cod] -tempest.log diff --git a/charms/glance-k8s/.gitreview b/charms/glance-k8s/.gitreview deleted file mode 100644 index 48ce75b3..00000000 --- a/charms/glance-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-glance-k8s.git -defaultbranch=main diff --git a/charms/glance-k8s/.jujuignore b/charms/glance-k8s/.jujuignore deleted file mode 100644 index 6ccd559e..00000000 --- a/charms/glance-k8s/.jujuignore +++ /dev/null @@ -1,3 +0,0 @@ -/venv -*.py[cod] -*.charm diff --git a/charms/glance-k8s/.stestr.conf b/charms/glance-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/glance-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/glance-k8s/.zuul.yaml b/charms/glance-k8s/.zuul.yaml deleted file mode 100644 index 38b2ac8d..00000000 --- a/charms/glance-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: glance-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/glance-k8s/charmcraft.yaml b/charms/glance-k8s/charmcraft.yaml index 2fdc318d..9556149e 100644 --- a/charms/glance-k8s/charmcraft.yaml +++ b/charms/glance-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/glance-k8s/fetch-libs.sh b/charms/glance-k8s/fetch-libs.sh deleted file mode 100755 index e7772471..00000000 --- a/charms/glance-k8s/fetch-libs.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.data_platform_libs.v0.database_requires -charmcraft fetch-lib charms.keystone_k8s.v1.identity_service -charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.traefik_k8s.v2.ingress diff --git a/charms/glance-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/glance-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 53d61912..00000000 --- a/charms/glance-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,496 +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. - -"""Relation 'requires' side abstraction for database relation. - -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.database_requires import 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.database_requires import 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, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(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 password(self) -> Optional[str]: - """Returns the password for the created user.""" - return self.relation.data[self.relation.app].get("password") - - @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 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") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - return self.relation.data[self.relation.app].get("username") - - @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(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(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) - - -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 — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() - - 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) - self.charm = charm - self.database = database_name - 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.relations_aliases = relations_aliases - 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 - ) - - # 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 _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. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].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] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - 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 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) - - 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") - - @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]) diff --git a/charms/glance-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/glance-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3f..00000000 --- a/charms/glance-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -Also provide additional parameters to the charm object: - - service - - internal_url - - public_url - - admin_url - - region - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - service = "my-service" - internal_url = "http://internal-url" - public_url = "http://public-url" - admin_url = "http://admin-url" - region = "region" - ) - self.framework.observe( - self.identity_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# 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 - - -logger = logging.getLogger(__name__) - - -class IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._identity_service_rel.data[self._identity_service_rel.app] - return data.get(key) - - @property - def api_version(self) -> str: - """Return the api_version.""" - return self.get_remote_app_data('api-version') - - @property - def auth_host(self) -> str: - """Return the auth_host.""" - return self.get_remote_app_data('auth-host') - - @property - def auth_port(self) -> str: - """Return the auth_port.""" - return self.get_remote_app_data('auth-port') - - @property - def auth_protocol(self) -> str: - """Return the auth_protocol.""" - return self.get_remote_app_data('auth-protocol') - - @property - def internal_host(self) -> str: - """Return the internal_host.""" - return self.get_remote_app_data('internal-host') - - @property - def internal_port(self) -> str: - """Return the internal_port.""" - return self.get_remote_app_data('internal-port') - - @property - def internal_protocol(self) -> str: - """Return the internal_protocol.""" - return self.get_remote_app_data('internal-protocol') - - @property - def admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("password") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("username") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_credentials(self, relation_name: int, - relation_id: str, - api_version: str, - auth_host: str, - auth_port: str, - auth_protocol: str, - internal_host: str, - internal_port: str, - internal_protocol: str, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_rel.data[self.charm.app] - app_data["api-version"] = api_version - app_data["auth-host"] = auth_host - app_data["auth-port"] = str(auth_port) - app_data["auth-protocol"] = auth_protocol - app_data["internal-host"] = internal_host - app_data["internal-port"] = str(internal_port) - app_data["internal-protocol"] = internal_protocol - app_data["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/charms/glance-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/glance-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/glance-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/glance-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/glance-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/glance-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/glance-k8s/osci.yaml b/charms/glance-k8s/osci.yaml deleted file mode 100644 index 538d747e..00000000 --- a/charms/glance-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: glance-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/glance-k8s/pyproject.toml b/charms/glance-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/glance-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/glance-k8s/rename.sh b/charms/glance-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/glance-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/glance-k8s/requirements.txt b/charms/glance-k8s/requirements.txt index c9d578be..451445a1 100644 --- a/charms/glance-k8s/requirements.txt +++ b/charms/glance-k8s/requirements.txt @@ -13,11 +13,12 @@ lightkube-models ops netifaces -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam - git+https://opendev.org/openstack/charm-ops-interface-tls-certificates#egg=interface_tls_certificates # Note: Required for cinder-ceph-k8s, glance-k8s git+https://opendev.org/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client # Charmhelpers is only present as interface_ceph_client uses it. git+https://github.com/juju/charm-helpers.git#egg=charmhelpers + +# From ops_sunbeam +tenacity diff --git a/charms/glance-k8s/src/charm.py b/charms/glance-k8s/src/charm.py index 62c6eb03..106207c0 100755 --- a/charms/glance-k8s/src/charm.py +++ b/charms/glance-k8s/src/charm.py @@ -338,7 +338,8 @@ class GlanceOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): def configure_charm(self, event) -> None: """Catchall handler to configure charm services.""" - if not self.relation_handlers_ready(): + not_ready_relations = self.get_mandatory_relations_not_ready(event) + if not_ready_relations: logger.debug("Deferring configuration, charm relations not ready") return diff --git a/charms/glance-k8s/src/templates/parts/section-database b/charms/glance-k8s/src/templates/parts/section-database deleted file mode 100644 index eb52f65e..00000000 --- a/charms/glance-k8s/src/templates/parts/section-database +++ /dev/null @@ -1,7 +0,0 @@ -[database] -{% if database.connection -%} -connection = {{ database.connection }} -{% else -%} -connection = sqlite:////var/lib/cinder/cinder.db -{% endif -%} -connection_recycle_time = 200 diff --git a/charms/glance-k8s/src/templates/parts/section-federation b/charms/glance-k8s/src/templates/parts/section-federation deleted file mode 100644 index 65ee99ed..00000000 --- a/charms/glance-k8s/src/templates/parts/section-federation +++ /dev/null @@ -1,10 +0,0 @@ -{% if trusted_dashboards %} -[federation] -{% for dashboard_url in trusted_dashboards -%} -trusted_dashboard = {{ dashboard_url }} -{% endfor -%} -{% endif %} -{% for sp in fid_sps -%} -[{{ sp['protocol-name'] }}] -remote_id_attribute = {{ sp['remote-id-attribute'] }} -{% endfor -%} diff --git a/charms/glance-k8s/src/templates/parts/section-identity b/charms/glance-k8s/src/templates/parts/section-identity deleted file mode 100644 index cbb1d069..00000000 --- a/charms/glance-k8s/src/templates/parts/section-identity +++ /dev/null @@ -1,24 +0,0 @@ -[keystone_authtoken] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True diff --git a/charms/glance-k8s/src/templates/parts/section-middleware b/charms/glance-k8s/src/templates/parts/section-middleware deleted file mode 100644 index e65f1d98..00000000 --- a/charms/glance-k8s/src/templates/parts/section-middleware +++ /dev/null @@ -1,6 +0,0 @@ -{% for section in sections -%} -[{{section}}] -{% for key, value in sections[section].items() -%} -{{ key }} = {{ value }} -{% endfor %} -{%- endfor %} diff --git a/charms/glance-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/glance-k8s/src/templates/parts/section-oslo-messaging-rabbit deleted file mode 100644 index 145c4ee9..00000000 --- a/charms/glance-k8s/src/templates/parts/section-oslo-messaging-rabbit +++ /dev/null @@ -1,2 +0,0 @@ -[oslo_messaging_rabbit] -rabbit_quorum_queue = True diff --git a/charms/glance-k8s/src/templates/parts/section-oslo-notifications b/charms/glance-k8s/src/templates/parts/section-oslo-notifications deleted file mode 100644 index ce559feb..00000000 --- a/charms/glance-k8s/src/templates/parts/section-oslo-notifications +++ /dev/null @@ -1,4 +0,0 @@ -{% if options.enable_telemetry_notifications -%} -[oslo_messaging_notifications] -driver = messagingv2 -{%- endif %} diff --git a/charms/glance-k8s/src/templates/parts/section-service-user b/charms/glance-k8s/src/templates/parts/section-service-user deleted file mode 100644 index 65103693..00000000 --- a/charms/glance-k8s/src/templates/parts/section-service-user +++ /dev/null @@ -1,17 +0,0 @@ -{% if identity_service.service_domain_id -%} -[service_user] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -send_service_user_token = true -auth_type = password -project_domain_id = {{ identity_service.service_domain_id }} -user_domain_id = {{ identity_service.service_domain_id }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -{% endif -%} diff --git a/charms/glance-k8s/src/templates/parts/section-signing b/charms/glance-k8s/src/templates/parts/section-signing deleted file mode 100644 index cb7d69ae..00000000 --- a/charms/glance-k8s/src/templates/parts/section-signing +++ /dev/null @@ -1,15 +0,0 @@ -{% if enable_signing -%} -[signing] -{% if certfile -%} -certfile = {{ certfile }} -{% endif -%} -{% if keyfile -%} -keyfile = {{ keyfile }} -{% endif -%} -{% if ca_certs -%} -ca_certs = {{ ca_certs }} -{% endif -%} -{% if ca_key -%} -ca_key = {{ ca_key }} -{% endif -%} -{% endif -%} \ No newline at end of file diff --git a/charms/glance-k8s/test-requirements.txt b/charms/glance-k8s/test-requirements.txt deleted file mode 100644 index a9b0d698..00000000 --- a/charms/glance-k8s/test-requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -coverage -mock -flake8 -stestr -git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza -git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack -git+https://opendev.org/openstack/tempest.git#egg=tempest -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/glance-k8s/tests/unit/test_glance_charm.py b/charms/glance-k8s/tests/unit/test_glance_charm.py index 3cd6887c..8d5c3914 100644 --- a/charms/glance-k8s/tests/unit/test_glance_charm.py +++ b/charms/glance-k8s/tests/unit/test_glance_charm.py @@ -16,13 +16,12 @@ """Tests for glance charm.""" +import charm import ops_sunbeam.test_utils as test_utils from mock import ( patch, ) -import charm - class _GlanceOperatorCharm(charm.GlanceOperatorCharm): def __init__(self, framework): diff --git a/charms/glance-k8s/tox.ini b/charms/glance-k8s/tox.ini deleted file mode 100644 index a411adb3..00000000 --- a/charms/glance-k8s/tox.ini +++ /dev/null @@ -1,160 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/gnocchi-k8s/.gitignore b/charms/gnocchi-k8s/.gitignore deleted file mode 100644 index 7d5f287a..00000000 --- a/charms/gnocchi-k8s/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -venv/ -build/ -*.charm -.tox/ -.coverage -__pycache__/ -*.py[cod] -.idea -.vscode/ -*.swp -.stestr/ - diff --git a/charms/gnocchi-k8s/.gitreview b/charms/gnocchi-k8s/.gitreview deleted file mode 100644 index 9b24204b..00000000 --- a/charms/gnocchi-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-gnocchi-k8s.git -defaultbranch=main diff --git a/charms/gnocchi-k8s/.stestr.conf b/charms/gnocchi-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/gnocchi-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/gnocchi-k8s/.zuul.yaml b/charms/gnocchi-k8s/.zuul.yaml deleted file mode 100644 index 4daf525d..00000000 --- a/charms/gnocchi-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: gnocchi-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.26-strict/stable - microk8s_classic_mode: false diff --git a/charms/gnocchi-k8s/charmcraft.yaml b/charms/gnocchi-k8s/charmcraft.yaml index 2fdc318d..9556149e 100644 --- a/charms/gnocchi-k8s/charmcraft.yaml +++ b/charms/gnocchi-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/gnocchi-k8s/fetch-libs.sh b/charms/gnocchi-k8s/fetch-libs.sh deleted file mode 100755 index e7772471..00000000 --- a/charms/gnocchi-k8s/fetch-libs.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.data_platform_libs.v0.database_requires -charmcraft fetch-lib charms.keystone_k8s.v1.identity_service -charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.traefik_k8s.v2.ingress diff --git a/charms/gnocchi-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/gnocchi-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 11ffd6ca..00000000 --- a/charms/gnocchi-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,537 +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. - -r"""[DEPRECATED] Relation 'requires' side abstraction for database relation. - -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.database_requires 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.database_requires 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, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 = 6 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(RelationEvent): - """Base class for database events.""" - - @property - def endpoints(self) -> Optional[str]: - """Returns a comma separated list of read/write endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("endpoints") - - @property - def password(self) -> Optional[str]: - """Returns the password for the created user.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("password") - - @property - def read_only_endpoints(self) -> Optional[str]: - """Returns a comma separated list of read only endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("read-only-endpoints") - - @property - def replset(self) -> Optional[str]: - """Returns the replicaset name. - - MongoDB only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("replset") - - @property - def tls(self) -> Optional[str]: - """Returns whether TLS is configured.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls") - - @property - def tls_ca(self) -> Optional[str]: - """Returns TLS CA.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls-ca") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("username") - - @property - def version(self) -> Optional[str]: - """Returns the version of the database. - - Version as informed by the database daemon. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("version") - - -class DatabaseCreatedEvent(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(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) - - -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 — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() # pyright: ignore [reportGeneralTypeIssues] - - def __init__( - self, - charm, - relation_name: str, - database_name: str, - extra_user_roles: Optional[str] = None, - relations_aliases: Optional[List[str]] = None, - ): - """Manager of database client relations.""" - super().__init__(charm, relation_name) - self.charm = charm - self.database = database_name - 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.relations_aliases = relations_aliases - 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 - ) - - # 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 _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. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].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"} - if event.app - else {} - ) - - # 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] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - 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 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"} - if relation.app - else {} - ) - 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 _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()) - getattr(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()) - getattr(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()) - getattr(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") - - @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]) diff --git a/charms/gnocchi-k8s/lib/charms/gnocchi_k8s/v0/gnocchi_service.py b/charms/gnocchi-k8s/lib/charms/gnocchi_k8s/v0/gnocchi_service.py deleted file mode 100644 index fbc679d5..00000000 --- a/charms/gnocchi-k8s/lib/charms/gnocchi_k8s/v0/gnocchi_service.py +++ /dev/null @@ -1,205 +0,0 @@ -"""GnocchiService Provides and Requires module. - -This library contains the Requires and Provides classes for handling -the Gnocchi service interface. - -Import `GnocchiServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "gnocchi-db" - -Two events are also available to respond to: - - readiness_changed - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.gnocchi_k8s.v0.gnocchi_service import ( - GnocchiServiceRequires -) - -class GnocchiServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # GnocchiService Requires - self.gnocchi_svc = GnocchiServiceRequires( - self, "gnocchi-db", - ) - self.framework.observe( - self.gnocchi_svc.on.readiness_changed, - self._on_gnocchi_service_readiness_changed - ) - self.framework.observe( - self.gnocchi_svc.on.goneaway, - self._on_gnocchi_service_goneaway - ) - - def _on_gnocchi_service_readiness_changed(self, event): - '''React to the Gnocchi service readiness changed event. - - This event happens when Gnocchi service relation is added to the - model and relation data is changed. - ''' - # Do something with the configuration provided by relation. - pass - - def _on_gnocchi_service_goneaway(self, event): - '''React to the Gnocchi Service goneaway event. - - This event happens when Gnocchi service relation is removed. - ''' - # HeatSharedConfig Relation has goneaway. - pass -``` -""" - - -import json -import logging -from typing import ( - Optional, -) - -from ops.charm import ( - CharmBase, - RelationBrokenEvent, - RelationChangedEvent, - RelationEvent, -) -from ops.framework import ( - EventSource, - Object, - ObjectEvents, -) -from ops.model import ( - Relation, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "97b7682b415040f3b32d77fff8d93e7e" - -# 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 = 2 - - -class GnocchiServiceReadinessRequestEvent(RelationEvent): - """GnocchiServiceReadinessRequest Event.""" - - pass - - -class GnocchiServiceProviderEvents(ObjectEvents): - """Events class for `on`.""" - - service_readiness = EventSource(GnocchiServiceReadinessRequestEvent) - - -class GnocchiServiceProvides(Object): - """GnocchiServiceProvides class.""" - - on = GnocchiServiceProviderEvents() - - def __init__(self, charm: CharmBase, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_relation_changed, - ) - - def _on_relation_changed(self, event: RelationChangedEvent): - """Handle Gnocchi service relation changed.""" - logging.debug("Gnocchi Service relation changed") - self.on.service_readiness.emit(event.relation) - - def set_service_status(self, relation: Relation, is_ready: bool) -> None: - """Set gnocchi service readiness status on the relation.""" - if not self.charm.unit.is_leader(): - logging.debug("Not a leader unit, skipping setting ready status") - return - - logging.debug( - f"Setting ready status on relation {relation.app.name} " - f"{relation.name}/{relation.id}" - ) - relation.data[self.charm.app]["ready"] = json.dumps(is_ready) - - -class GnocchiServiceReadinessChangedEvent(RelationEvent): - """GnocchiServiceReadinessChanged Event.""" - - pass - - -class GnocchiServiceGoneAwayEvent(RelationEvent): - """GnocchiServiceGoneAway Event.""" - - pass - - -class GnocchiServiceRequirerEvents(ObjectEvents): - """Events class for `on`.""" - - readiness_changed = EventSource(GnocchiServiceReadinessChangedEvent) - goneaway = EventSource(GnocchiServiceGoneAwayEvent) - - -class GnocchiServiceRequires(Object): - """GnocchiServiceRequires class.""" - - on = GnocchiServiceRequirerEvents() - - def __init__(self, charm: CharmBase, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_relation_broken, - ) - - def _on_relation_changed(self, event: RelationChangedEvent): - """Handle Gnocchi Service relation changed.""" - logging.debug("Gnocchi service readiness data changed") - self.on.readiness_changed.emit(event.relation) - - def _on_relation_broken(self, event: RelationBrokenEvent): - """Handle Gnocchi Service relation broken.""" - logging.debug("Gnocchi service on_broken") - self.on.goneaway.emit(event.relation) - - @property - def _gnocchi_service_rel(self) -> Optional[Relation]: - """The gnocchi service relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> Optional[str]: - """Return the value for the given key from remote app data.""" - if self._gnocchi_service_rel: - data = self._gnocchi_service_rel.data[ - self._gnocchi_service_rel.app - ] - return data.get(key) - - return None - - @property - def service_ready(self) -> bool: - """Return if gnocchi service is ready or not.""" - is_ready = self.get_remote_app_data("ready") - if is_ready: - return json.loads(is_ready) - - return False diff --git a/charms/gnocchi-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/gnocchi-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3f..00000000 --- a/charms/gnocchi-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -Also provide additional parameters to the charm object: - - service - - internal_url - - public_url - - admin_url - - region - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - service = "my-service" - internal_url = "http://internal-url" - public_url = "http://public-url" - admin_url = "http://admin-url" - region = "region" - ) - self.framework.observe( - self.identity_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# 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 - - -logger = logging.getLogger(__name__) - - -class IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._identity_service_rel.data[self._identity_service_rel.app] - return data.get(key) - - @property - def api_version(self) -> str: - """Return the api_version.""" - return self.get_remote_app_data('api-version') - - @property - def auth_host(self) -> str: - """Return the auth_host.""" - return self.get_remote_app_data('auth-host') - - @property - def auth_port(self) -> str: - """Return the auth_port.""" - return self.get_remote_app_data('auth-port') - - @property - def auth_protocol(self) -> str: - """Return the auth_protocol.""" - return self.get_remote_app_data('auth-protocol') - - @property - def internal_host(self) -> str: - """Return the internal_host.""" - return self.get_remote_app_data('internal-host') - - @property - def internal_port(self) -> str: - """Return the internal_port.""" - return self.get_remote_app_data('internal-port') - - @property - def internal_protocol(self) -> str: - """Return the internal_protocol.""" - return self.get_remote_app_data('internal-protocol') - - @property - def admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("password") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("username") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_credentials(self, relation_name: int, - relation_id: str, - api_version: str, - auth_host: str, - auth_port: str, - auth_protocol: str, - internal_host: str, - internal_port: str, - internal_protocol: str, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_rel.data[self.charm.app] - app_data["api-version"] = api_version - app_data["auth-host"] = auth_host - app_data["auth-port"] = str(auth_port) - app_data["auth-protocol"] = auth_protocol - app_data["internal-host"] = internal_host - app_data["internal-port"] = str(internal_port) - app_data["internal-protocol"] = internal_protocol - app_data["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/charms/gnocchi-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/gnocchi-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/gnocchi-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/gnocchi-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/gnocchi-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/gnocchi-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/gnocchi-k8s/osci.yaml b/charms/gnocchi-k8s/osci.yaml deleted file mode 100644 index 05207373..00000000 --- a/charms/gnocchi-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: gnocchi-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/gnocchi-k8s/pyproject.toml b/charms/gnocchi-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/gnocchi-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/gnocchi-k8s/rename.sh b/charms/gnocchi-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/gnocchi-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/gnocchi-k8s/requirements.txt b/charms/gnocchi-k8s/requirements.txt index b932d999..6a4bf90a 100644 --- a/charms/gnocchi-k8s/requirements.txt +++ b/charms/gnocchi-k8s/requirements.txt @@ -1,6 +1,5 @@ ops jinja2 -git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam lightkube netifaces jsonschema @@ -8,3 +7,6 @@ pydantic<2.0 git+https://github.com/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client git+https://github.com/juju/charm-helpers.git#egg=charmhelpers + +# From ops_sunbeam +tenacity diff --git a/charms/gnocchi-k8s/src/templates/parts/database-connection b/charms/gnocchi-k8s/src/templates/parts/database-connection deleted file mode 100644 index 1fd70ce2..00000000 --- a/charms/gnocchi-k8s/src/templates/parts/database-connection +++ /dev/null @@ -1,3 +0,0 @@ -{% if database.connection -%} -connection = {{ database.connection }} -{% endif -%} diff --git a/charms/gnocchi-k8s/src/templates/parts/identity-data b/charms/gnocchi-k8s/src/templates/parts/identity-data deleted file mode 100644 index 706d9d13..00000000 --- a/charms/gnocchi-k8s/src/templates/parts/identity-data +++ /dev/null @@ -1,23 +0,0 @@ -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True diff --git a/charms/gnocchi-k8s/src/templates/parts/section-database b/charms/gnocchi-k8s/src/templates/parts/section-database deleted file mode 100644 index 986d9b10..00000000 --- a/charms/gnocchi-k8s/src/templates/parts/section-database +++ /dev/null @@ -1,3 +0,0 @@ -[database] -{% include "parts/database-connection" %} -connection_recycle_time = 200 diff --git a/charms/gnocchi-k8s/src/templates/parts/section-federation b/charms/gnocchi-k8s/src/templates/parts/section-federation deleted file mode 100644 index 65ee99ed..00000000 --- a/charms/gnocchi-k8s/src/templates/parts/section-federation +++ /dev/null @@ -1,10 +0,0 @@ -{% if trusted_dashboards %} -[federation] -{% for dashboard_url in trusted_dashboards -%} -trusted_dashboard = {{ dashboard_url }} -{% endfor -%} -{% endif %} -{% for sp in fid_sps -%} -[{{ sp['protocol-name'] }}] -remote_id_attribute = {{ sp['remote-id-attribute'] }} -{% endfor -%} diff --git a/charms/gnocchi-k8s/src/templates/parts/section-identity b/charms/gnocchi-k8s/src/templates/parts/section-identity deleted file mode 100644 index 7568a9a4..00000000 --- a/charms/gnocchi-k8s/src/templates/parts/section-identity +++ /dev/null @@ -1,2 +0,0 @@ -[keystone_authtoken] -{% include "parts/identity-data" %} diff --git a/charms/gnocchi-k8s/src/templates/parts/section-middleware b/charms/gnocchi-k8s/src/templates/parts/section-middleware deleted file mode 100644 index e65f1d98..00000000 --- a/charms/gnocchi-k8s/src/templates/parts/section-middleware +++ /dev/null @@ -1,6 +0,0 @@ -{% for section in sections -%} -[{{section}}] -{% for key, value in sections[section].items() -%} -{{ key }} = {{ value }} -{% endfor %} -{%- endfor %} diff --git a/charms/gnocchi-k8s/src/templates/parts/section-signing b/charms/gnocchi-k8s/src/templates/parts/section-signing deleted file mode 100644 index cb7d69ae..00000000 --- a/charms/gnocchi-k8s/src/templates/parts/section-signing +++ /dev/null @@ -1,15 +0,0 @@ -{% if enable_signing -%} -[signing] -{% if certfile -%} -certfile = {{ certfile }} -{% endif -%} -{% if keyfile -%} -keyfile = {{ keyfile }} -{% endif -%} -{% if ca_certs -%} -ca_certs = {{ ca_certs }} -{% endif -%} -{% if ca_key -%} -ca_key = {{ ca_key }} -{% endif -%} -{% endif -%} \ No newline at end of file diff --git a/charms/gnocchi-k8s/test-requirements.txt b/charms/gnocchi-k8s/test-requirements.txt deleted file mode 100644 index 276e5bee..00000000 --- a/charms/gnocchi-k8s/test-requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -# This file is managed centrally. If you find the need to modify this as a -# one-off, please don't. Intead, consult #openstack-charms and ask about -# requirements management in charms via bot-control. Thank you. - -coverage -mock -flake8 -stestr -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/gnocchi-k8s/tests/unit/test_charm.py b/charms/gnocchi-k8s/tests/unit/test_charm.py index 2d5da5cc..f0ac97d7 100644 --- a/charms/gnocchi-k8s/tests/unit/test_charm.py +++ b/charms/gnocchi-k8s/tests/unit/test_charm.py @@ -16,13 +16,12 @@ """Tests for gnocchi charm.""" +import charm import ops_sunbeam.test_utils as test_utils from mock import ( patch, ) -import charm - class _GnocchiCephOperatorCharm(charm.GnocchiCephOperatorCharm): def __init__(self, framework): diff --git a/charms/gnocchi-k8s/tox.ini b/charms/gnocchi-k8s/tox.ini deleted file mode 100644 index 067963f9..00000000 --- a/charms/gnocchi-k8s/tox.ini +++ /dev/null @@ -1,165 +0,0 @@ -# Operator charm (with zaza): tox.ini - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - HOME - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/heat-k8s/.gitignore b/charms/heat-k8s/.gitignore deleted file mode 100644 index 73f116c9..00000000 --- a/charms/heat-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -*.charm -*.swp - -.coverage -__pycache__/ -*.py[cod] -.tox -.stestr/ -tempest.log diff --git a/charms/heat-k8s/.stestr.conf b/charms/heat-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/heat-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/heat-k8s/.zuul.yaml b/charms/heat-k8s/.zuul.yaml deleted file mode 100644 index cfece9f8..00000000 --- a/charms/heat-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: heat-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/heat-k8s/charmcraft.yaml b/charms/heat-k8s/charmcraft.yaml index 2fdc318d..9556149e 100644 --- a/charms/heat-k8s/charmcraft.yaml +++ b/charms/heat-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/heat-k8s/fetch-libs.sh b/charms/heat-k8s/fetch-libs.sh deleted file mode 100755 index 600c0b69..00000000 --- a/charms/heat-k8s/fetch-libs.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.data_platform_libs.v0.database_requires -charmcraft fetch-lib charms.keystone_k8s.v0.identity_resource -charmcraft fetch-lib charms.keystone_k8s.v1.identity_service -charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.traefik_route_k8s.v0.traefik_route diff --git a/charms/heat-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/heat-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 11ffd6ca..00000000 --- a/charms/heat-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,537 +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. - -r"""[DEPRECATED] Relation 'requires' side abstraction for database relation. - -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.database_requires 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.database_requires 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, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 = 6 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(RelationEvent): - """Base class for database events.""" - - @property - def endpoints(self) -> Optional[str]: - """Returns a comma separated list of read/write endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("endpoints") - - @property - def password(self) -> Optional[str]: - """Returns the password for the created user.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("password") - - @property - def read_only_endpoints(self) -> Optional[str]: - """Returns a comma separated list of read only endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("read-only-endpoints") - - @property - def replset(self) -> Optional[str]: - """Returns the replicaset name. - - MongoDB only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("replset") - - @property - def tls(self) -> Optional[str]: - """Returns whether TLS is configured.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls") - - @property - def tls_ca(self) -> Optional[str]: - """Returns TLS CA.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls-ca") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("username") - - @property - def version(self) -> Optional[str]: - """Returns the version of the database. - - Version as informed by the database daemon. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("version") - - -class DatabaseCreatedEvent(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(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) - - -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 — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() # pyright: ignore [reportGeneralTypeIssues] - - def __init__( - self, - charm, - relation_name: str, - database_name: str, - extra_user_roles: Optional[str] = None, - relations_aliases: Optional[List[str]] = None, - ): - """Manager of database client relations.""" - super().__init__(charm, relation_name) - self.charm = charm - self.database = database_name - 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.relations_aliases = relations_aliases - 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 - ) - - # 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 _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. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].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"} - if event.app - else {} - ) - - # 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] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - 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 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"} - if relation.app - else {} - ) - 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 _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()) - getattr(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()) - getattr(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()) - getattr(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") - - @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]) diff --git a/charms/heat-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/heat-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3f..00000000 --- a/charms/heat-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -Also provide additional parameters to the charm object: - - service - - internal_url - - public_url - - admin_url - - region - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - service = "my-service" - internal_url = "http://internal-url" - public_url = "http://public-url" - admin_url = "http://admin-url" - region = "region" - ) - self.framework.observe( - self.identity_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# 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 - - -logger = logging.getLogger(__name__) - - -class IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._identity_service_rel.data[self._identity_service_rel.app] - return data.get(key) - - @property - def api_version(self) -> str: - """Return the api_version.""" - return self.get_remote_app_data('api-version') - - @property - def auth_host(self) -> str: - """Return the auth_host.""" - return self.get_remote_app_data('auth-host') - - @property - def auth_port(self) -> str: - """Return the auth_port.""" - return self.get_remote_app_data('auth-port') - - @property - def auth_protocol(self) -> str: - """Return the auth_protocol.""" - return self.get_remote_app_data('auth-protocol') - - @property - def internal_host(self) -> str: - """Return the internal_host.""" - return self.get_remote_app_data('internal-host') - - @property - def internal_port(self) -> str: - """Return the internal_port.""" - return self.get_remote_app_data('internal-port') - - @property - def internal_protocol(self) -> str: - """Return the internal_protocol.""" - return self.get_remote_app_data('internal-protocol') - - @property - def admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("password") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("username") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_credentials(self, relation_name: int, - relation_id: str, - api_version: str, - auth_host: str, - auth_port: str, - auth_protocol: str, - internal_host: str, - internal_port: str, - internal_protocol: str, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_rel.data[self.charm.app] - app_data["api-version"] = api_version - app_data["auth-host"] = auth_host - app_data["auth-port"] = str(auth_port) - app_data["auth-protocol"] = auth_protocol - app_data["internal-host"] = internal_host - app_data["internal-port"] = str(internal_port) - app_data["internal-protocol"] = internal_protocol - app_data["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/charms/heat-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py b/charms/heat-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py deleted file mode 100644 index 4cf26164..00000000 --- a/charms/heat-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py +++ /dev/null @@ -1,408 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# Licensed under the Apache2.0, see LICENCE file in charm source for details. -"""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: -- additional-hostnames -- backend-protocol -- limit-rps -- limit-whitelist -- max-body-size -- owasp-modsecurity-crs -- owasp-modsecurity-custom-rules -- path-routes -- retry-errors -- rewrite-enabled -- rewrite-target -- service-hostname (required) -- service-name (required) -- service-namespace -- service-port (required) -- 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, for the relation -changed event to be properly handled. -""" - -import copy -import logging -from typing import Dict - -from ops.charm import CharmBase, CharmEvents, RelationBrokenEvent, RelationChangedEvent -from ops.framework import EventBase, EventSource, Object -from ops.model import BlockedStatus - -INGRESS_RELATION_NAME = "ingress" -INGRESS_PROXY_RELATION_NAME = "ingress-proxy" - -# 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 = 16 - -LOGGER = logging.getLogger(__name__) - -REQUIRED_INGRESS_RELATION_FIELDS = {"service-hostname", "service-name", "service-port"} - -OPTIONAL_INGRESS_RELATION_FIELDS = { - "additional-hostnames", - "backend-protocol", - "limit-rps", - "limit-whitelist", - "max-body-size", - "owasp-modsecurity-crs", - "owasp-modsecurity-custom-rules", - "path-routes", - "retry-errors", - "rewrite-target", - "rewrite-enabled", - "service-namespace", - "session-cookie-max-age", - "tls-secret-name", -} - -RELATION_INTERFACES_MAPPINGS = { - "service-hostname": "host", - "service-name": "name", - "service-namespace": "model", - "service-port": "port", -} -RELATION_INTERFACES_MAPPINGS_VALUES = set(RELATION_INTERFACES_MAPPINGS.values()) - - -class IngressAvailableEvent(EventBase): - """IngressAvailableEvent custom event. - - This event indicates the Ingress provider is available. - """ - - -class IngressProxyAvailableEvent(EventBase): - """IngressProxyAvailableEvent custom event. - - This event indicates the IngressProxy provider is available. - """ - - -class IngressBrokenEvent(RelationBrokenEvent): - """IngressBrokenEvent custom event. - - This event indicates the Ingress provider is broken. - """ - - -class IngressCharmEvents(CharmEvents): - """Custom charm events. - - Attrs: - ingress_available: Event to indicate that Ingress is available. - ingress_proxy_available: Event to indicate that IngressProxy is available. - ingress_broken: Event to indicate that Ingress is broken. - """ - - ingress_available = EventSource(IngressAvailableEvent) - ingress_proxy_available = EventSource(IngressProxyAvailableEvent) - 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 - - Attrs: - model: Juju model where the charm is deployed. - config_dict: Contains all the configuration options for Ingress. - """ - - def __init__(self, charm: CharmBase, config_dict: Dict) -> None: - """Init function for the IngressRequires class. - - Args: - charm: The charm that requires the ingress relation. - config_dict: Contains all the configuration options for Ingress. - """ - super().__init__(charm, INGRESS_RELATION_NAME) - - self.framework.observe( - charm.on[INGRESS_RELATION_NAME].relation_changed, self._on_relation_changed - ) - - # Set default values. - default_relation_fields = { - "service-namespace": self.model.name, - } - config_dict.update( - (key, value) - for key, value in default_relation_fields.items() - if key not in config_dict or not config_dict[key] - ) - - self.config_dict = self._convert_to_relation_interface(config_dict) - - @staticmethod - def _convert_to_relation_interface(config_dict: Dict) -> Dict: - """Create a new relation dict that conforms with charm-relation-interfaces. - - Args: - config_dict: Ingress configuration that doesn't conform with charm-relation-interfaces. - - Returns: - The Ingress configuration conforming with charm-relation-interfaces. - """ - config_dict = copy.copy(config_dict) - config_dict.update( - (key, config_dict[old_key]) - for old_key, key in RELATION_INTERFACES_MAPPINGS.items() - if old_key in config_dict and config_dict[old_key] - ) - return config_dict - - def _config_dict_errors(self, config_dict: Dict, update_only: bool = False) -> bool: - """Check our config dict for errors. - - Args: - config_dict: Contains all the configuration options for Ingress. - update_only: If the charm needs to update only existing keys. - - Returns: - If we need to update the config dict or not. - """ - blocked_message = "Error in ingress relation, check `juju debug-log`" - unknown = [ - config_key - for config_key in config_dict - if config_key - not in REQUIRED_INGRESS_RELATION_FIELDS - | OPTIONAL_INGRESS_RELATION_FIELDS - | RELATION_INTERFACES_MAPPINGS_VALUES - ] - 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 = tuple( - config_key - for config_key in REQUIRED_INGRESS_RELATION_FIELDS - if config_key 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: RelationChangedEvent) -> None: - """Handle the relation-changed event. - - Args: - event: Event triggering the relation-changed hook for the relation. - """ - # `self.unit` isn't available here, so use `self.model.unit`. - if self.model.unit.is_leader(): - if self._config_dict_errors(config_dict=self.config_dict): - return - event.relation.data[self.model.app].update( - (key, str(self.config_dict[key])) for key in self.config_dict - ) - - def update_config(self, config_dict: Dict) -> None: - """Allow for updates to relation. - - Args: - config_dict: Contains all the configuration options for Ingress. - - Attrs: - config_dict: Contains all the configuration options for Ingress. - """ - if self.model.unit.is_leader(): - self.config_dict = self._convert_to_relation_interface(config_dict) - if self._config_dict_errors(self.config_dict, update_only=True): - return - relation = self.model.get_relation(INGRESS_RELATION_NAME) - if relation: - for key in self.config_dict: - relation.data[self.model.app][key] = str(self.config_dict[key]) - - -class IngressBaseProvides(Object): - """Parent class for IngressProvides and IngressProxyProvides. - - Attrs: - model: Juju model where the charm is deployed. - """ - - def __init__(self, charm: CharmBase, relation_name: str) -> None: - """Init function for the IngressProxyProvides class. - - Args: - charm: The charm that provides the ingress-proxy relation. - """ - super().__init__(charm, relation_name) - self.charm = charm - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handle a change to the ingress/ingress-proxy relation. - - Confirm we have the fields we expect to receive. - - Args: - event: Event triggering the relation-changed hook for the relation. - """ - # `self.unit` isn't available here, so use `self.model.unit`. - if not self.model.unit.is_leader(): - return - - relation_name = event.relation.name - - assert event.app is not None # nosec - if not event.relation.data[event.app]: - LOGGER.info( - "%s hasn't finished configuring, waiting until relation is changed again.", - relation_name, - ) - 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.warning( - "Missing required data fields for %s relation: %s", - relation_name, - ", ".join(missing_fields), - ) - self.model.unit.status = BlockedStatus( - f"Missing fields for {relation_name}: {', '.join(missing_fields)}" - ) - - if relation_name == INGRESS_RELATION_NAME: - # Conform to charm-relation-interfaces. - if "name" in ingress_data and "port" in ingress_data: - name = ingress_data["name"] - port = ingress_data["port"] - else: - name = ingress_data["service-name"] - port = ingress_data["service-port"] - event.relation.data[self.model.app]["url"] = f"http://{name}:{port}/" - - # Create an event that our charm can use to decide it's okay to - # configure the ingress. - self.charm.on.ingress_available.emit() - elif relation_name == INGRESS_PROXY_RELATION_NAME: - self.charm.on.ingress_proxy_available.emit() - - -class IngressProvides(IngressBaseProvides): - """Class containing the functionality for the 'provides' side of the 'ingress' relation. - - Attrs: - charm: The charm that provides the ingress relation. - - Hook events observed: - - relation-changed - """ - - def __init__(self, charm: CharmBase) -> None: - """Init function for the IngressProvides class. - - Args: - charm: The charm that provides the ingress relation. - """ - super().__init__(charm, INGRESS_RELATION_NAME) - # Observe the relation-changed hook event and bind - # self.on_relation_changed() to handle the event. - self.framework.observe( - charm.on[INGRESS_RELATION_NAME].relation_changed, self._on_relation_changed - ) - self.framework.observe( - charm.on[INGRESS_RELATION_NAME].relation_broken, self._on_relation_broken - ) - - def _on_relation_broken(self, event: RelationBrokenEvent) -> None: - """Handle a relation-broken event in the ingress relation. - - Args: - event: Event triggering the relation-broken hook for the 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(event.relation) - - -class IngressProxyProvides(IngressBaseProvides): - """Class containing the functionality for the 'provides' side of the 'ingress-proxy' relation. - - Attrs: - charm: The charm that provides the ingress-proxy relation. - - Hook events observed: - - relation-changed - """ - - def __init__(self, charm: CharmBase) -> None: - """Init function for the IngressProxyProvides class. - - Args: - charm: The charm that provides the ingress-proxy relation. - """ - super().__init__(charm, INGRESS_PROXY_RELATION_NAME) - # Observe the relation-changed hook event and bind - # self.on_relation_changed() to handle the event. - self.framework.observe( - charm.on[INGRESS_PROXY_RELATION_NAME].relation_changed, self._on_relation_changed - ) diff --git a/charms/heat-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/heat-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/heat-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/heat-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/heat-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/heat-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/heat-k8s/osci.yaml b/charms/heat-k8s/osci.yaml deleted file mode 100644 index a971062f..00000000 --- a/charms/heat-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: heat-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/heat-k8s/pyproject.toml b/charms/heat-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/heat-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/heat-k8s/rename.sh b/charms/heat-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/heat-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/heat-k8s/requirements.txt b/charms/heat-k8s/requirements.txt index 88f53cfa..9bf4ecff 100644 --- a/charms/heat-k8s/requirements.txt +++ b/charms/heat-k8s/requirements.txt @@ -1,7 +1,6 @@ ops jinja2 pwgen -git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam lightkube pydantic<2.0 @@ -9,3 +8,6 @@ pydantic<2.0 git+https://github.com/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client # Charmhelpers is only present as interface_ceph_client uses it. git+https://github.com/juju/charm-helpers.git#egg=charmhelpers + +# From ops_sunbeam +tenacity diff --git a/charms/heat-k8s/src/templates/parts/section-database b/charms/heat-k8s/src/templates/parts/section-database deleted file mode 100644 index eb52f65e..00000000 --- a/charms/heat-k8s/src/templates/parts/section-database +++ /dev/null @@ -1,7 +0,0 @@ -[database] -{% if database.connection -%} -connection = {{ database.connection }} -{% else -%} -connection = sqlite:////var/lib/cinder/cinder.db -{% endif -%} -connection_recycle_time = 200 diff --git a/charms/heat-k8s/src/templates/parts/section-federation b/charms/heat-k8s/src/templates/parts/section-federation deleted file mode 100644 index 65ee99ed..00000000 --- a/charms/heat-k8s/src/templates/parts/section-federation +++ /dev/null @@ -1,10 +0,0 @@ -{% if trusted_dashboards %} -[federation] -{% for dashboard_url in trusted_dashboards -%} -trusted_dashboard = {{ dashboard_url }} -{% endfor -%} -{% endif %} -{% for sp in fid_sps -%} -[{{ sp['protocol-name'] }}] -remote_id_attribute = {{ sp['remote-id-attribute'] }} -{% endfor -%} diff --git a/charms/heat-k8s/src/templates/parts/section-identity b/charms/heat-k8s/src/templates/parts/section-identity deleted file mode 100644 index d8b11646..00000000 --- a/charms/heat-k8s/src/templates/parts/section-identity +++ /dev/null @@ -1,38 +0,0 @@ -[keystone_authtoken] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True - - -[trustee] -auth_type = password -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -user_domain_name = {{ identity_service.service_domain_name }} diff --git a/charms/heat-k8s/src/templates/parts/section-middleware b/charms/heat-k8s/src/templates/parts/section-middleware deleted file mode 100644 index e65f1d98..00000000 --- a/charms/heat-k8s/src/templates/parts/section-middleware +++ /dev/null @@ -1,6 +0,0 @@ -{% for section in sections -%} -[{{section}}] -{% for key, value in sections[section].items() -%} -{{ key }} = {{ value }} -{% endfor %} -{%- endfor %} diff --git a/charms/heat-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/heat-k8s/src/templates/parts/section-oslo-messaging-rabbit deleted file mode 100644 index 145c4ee9..00000000 --- a/charms/heat-k8s/src/templates/parts/section-oslo-messaging-rabbit +++ /dev/null @@ -1,2 +0,0 @@ -[oslo_messaging_rabbit] -rabbit_quorum_queue = True diff --git a/charms/heat-k8s/src/templates/parts/section-signing b/charms/heat-k8s/src/templates/parts/section-signing deleted file mode 100644 index cb7d69ae..00000000 --- a/charms/heat-k8s/src/templates/parts/section-signing +++ /dev/null @@ -1,15 +0,0 @@ -{% if enable_signing -%} -[signing] -{% if certfile -%} -certfile = {{ certfile }} -{% endif -%} -{% if keyfile -%} -keyfile = {{ keyfile }} -{% endif -%} -{% if ca_certs -%} -ca_certs = {{ ca_certs }} -{% endif -%} -{% if ca_key -%} -ca_key = {{ ca_key }} -{% endif -%} -{% endif -%} \ No newline at end of file diff --git a/charms/heat-k8s/test-requirements.txt b/charms/heat-k8s/test-requirements.txt deleted file mode 100644 index 23e005bd..00000000 --- a/charms/heat-k8s/test-requirements.txt +++ /dev/null @@ -1,17 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -pwgen -coverage -mock -flake8 -stestr -git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza -git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack -git+https://opendev.org/openstack/tempest.git#egg=tempest -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/heat-k8s/tests/unit/test_heat_charm.py b/charms/heat-k8s/tests/unit/test_heat_charm.py index 05652c02..d230cb1b 100644 --- a/charms/heat-k8s/tests/unit/test_heat_charm.py +++ b/charms/heat-k8s/tests/unit/test_heat_charm.py @@ -22,13 +22,12 @@ from unittest.mock import ( Mock, ) +import charm import ops_sunbeam.test_utils as test_utils from ops.testing import ( Harness, ) -import charm - class _HeatTestOperatorCharm(charm.HeatOperatorCharm): """Test Operator Charm for Heat Operator.""" diff --git a/charms/heat-k8s/tox.ini b/charms/heat-k8s/tox.ini deleted file mode 100644 index 848869a4..00000000 --- a/charms/heat-k8s/tox.ini +++ /dev/null @@ -1,170 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - HOME - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest - extras -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/horizon-k8s/.flake8 b/charms/horizon-k8s/.flake8 deleted file mode 100644 index 8ef84fcd..00000000 --- a/charms/horizon-k8s/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 99 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/charms/horizon-k8s/.gitignore b/charms/horizon-k8s/.gitignore deleted file mode 100644 index 2b0b5a83..00000000 --- a/charms/horizon-k8s/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -venv/ -build/ -*.charm -.tox -.coverage -__pycache__/ -*.py[cod] -.stestr/ diff --git a/charms/horizon-k8s/.gitreview b/charms/horizon-k8s/.gitreview deleted file mode 100644 index dbdc5803..00000000 --- a/charms/horizon-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-horizon-k8s.git -defaultbranch=main diff --git a/charms/horizon-k8s/.jujuignore b/charms/horizon-k8s/.jujuignore deleted file mode 100644 index 6ccd559e..00000000 --- a/charms/horizon-k8s/.jujuignore +++ /dev/null @@ -1,3 +0,0 @@ -/venv -*.py[cod] -*.charm diff --git a/charms/horizon-k8s/.stestr.conf b/charms/horizon-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/horizon-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/horizon-k8s/.zuul.yaml b/charms/horizon-k8s/.zuul.yaml deleted file mode 100644 index fd20909e..00000000 --- a/charms/horizon-k8s/.zuul.yaml +++ /dev/null @@ -1,4 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs diff --git a/charms/horizon-k8s/charmcraft.yaml b/charms/horizon-k8s/charmcraft.yaml index 2fdc318d..9556149e 100644 --- a/charms/horizon-k8s/charmcraft.yaml +++ b/charms/horizon-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/horizon-k8s/fetch-libs.sh b/charms/horizon-k8s/fetch-libs.sh deleted file mode 100755 index a276fddb..00000000 --- a/charms/horizon-k8s/fetch-libs.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.data_platform_libs.v0.database_requires -charmcraft fetch-lib charms.keystone_k8s.v1.identity_service -charmcraft fetch-lib charms.keystone_k8s.v0.identity_credentials -charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.observability_libs.v1.kubernetes_service_patch -charmcraft fetch-lib charms.traefik_k8s.v2.ingress diff --git a/charms/horizon-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/horizon-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 53d61912..00000000 --- a/charms/horizon-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,496 +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. - -"""Relation 'requires' side abstraction for database relation. - -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.database_requires import 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.database_requires import 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, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(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 password(self) -> Optional[str]: - """Returns the password for the created user.""" - return self.relation.data[self.relation.app].get("password") - - @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 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") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - return self.relation.data[self.relation.app].get("username") - - @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(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(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) - - -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 — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() - - 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) - self.charm = charm - self.database = database_name - 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.relations_aliases = relations_aliases - 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 - ) - - # 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 _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. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].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] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - 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 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) - - 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") - - @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]) diff --git a/charms/horizon-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py b/charms/horizon-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py deleted file mode 100644 index 162a46a8..00000000 --- a/charms/horizon-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py +++ /dev/null @@ -1,439 +0,0 @@ -"""IdentityCredentialsProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_credentials interface. - -Import `IdentityCredentialsRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_credentials" - -Also provide additional parameters to the charm object: - - service - - internal_url - - public_url - - admin_url - - region - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v0.identity_credentials import IdentityCredentialsRequires - -class IdentityCredentialsClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityCredentials Requires - self.identity_credentials = IdentityCredentialsRequires( - self, "identity_credentials", - service = "my-service" - internal_url = "http://internal-url" - public_url = "http://public-url" - admin_url = "http://admin-url" - region = "region" - ) - self.framework.observe( - self.identity_credentials.on.connected, self._on_identity_credentials_connected) - self.framework.observe( - self.identity_credentials.on.ready, self._on_identity_credentials_ready) - self.framework.observe( - self.identity_credentials.on.goneaway, self._on_identity_credentials_goneaway) - - def _on_identity_credentials_connected(self, event): - '''React to the IdentityCredentials connected event. - - This event happens when IdentityCredentials relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_credentials_ready(self, event): - '''React to the IdentityCredentials ready event. - - The IdentityCredentials interface will use the provided config for the - request to the identity server. - ''' - # IdentityCredentials Relation is ready. Do something with the completed relation. - pass - - def _on_identity_credentials_goneaway(self, event): - '''React to the IdentityCredentials goneaway event. - - This event happens when an IdentityCredentials relation is removed. - ''' - # IdentityCredentials Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -# The unique Charmhub library identifier, never change it -LIBID = "b5fa18d4427c4ab9a269c3a2fbed545c" - -# 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 - -logger = logging.getLogger(__name__) - - -class IdentityCredentialsConnectedEvent(EventBase): - """IdentityCredentials connected Event.""" - - pass - - -class IdentityCredentialsReadyEvent(EventBase): - """IdentityCredentials ready for use Event.""" - - pass - - -class IdentityCredentialsGoneAwayEvent(EventBase): - """IdentityCredentials relation has gone-away Event""" - - pass - - -class IdentityCredentialsServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityCredentialsConnectedEvent) - ready = EventSource(IdentityCredentialsReadyEvent) - goneaway = EventSource(IdentityCredentialsGoneAwayEvent) - - -class IdentityCredentialsRequires(Object): - """ - IdentityCredentialsRequires class - """ - - on = IdentityCredentialsServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_credentials_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_credentials_relation_broken, - ) - - def _on_identity_credentials_relation_joined(self, event): - """IdentityCredentials relation joined.""" - logging.debug("IdentityCredentials on_joined") - self.on.connected.emit() - self.request_credentials() - - def _on_identity_credentials_relation_changed(self, event): - """IdentityCredentials relation changed.""" - logging.debug("IdentityCredentials on_changed") - try: - self.on.ready.emit() - except (AttributeError, KeyError): - logger.exception('Error when emitting event') - - def _on_identity_credentials_relation_broken(self, event): - """IdentityCredentials relation broken.""" - logging.debug("IdentityCredentials on_broken") - self.on.goneaway.emit() - - @property - def _identity_credentials_rel(self) -> Relation: - """The IdentityCredentials relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._identity_credentials_rel.data[self._identity_credentials_rel.app] - return data.get(key) - - @property - def api_version(self) -> str: - """Return the api_version.""" - return self.get_remote_app_data('api-version') - - @property - def auth_host(self) -> str: - """Return the auth_host.""" - return self.get_remote_app_data('auth-host') - - @property - def auth_port(self) -> str: - """Return the auth_port.""" - return self.get_remote_app_data('auth-port') - - @property - def auth_protocol(self) -> str: - """Return the auth_protocol.""" - return self.get_remote_app_data('auth-protocol') - - @property - def internal_host(self) -> str: - """Return the internal_host.""" - return self.get_remote_app_data('internal-host') - - @property - def internal_port(self) -> str: - """Return the internal_port.""" - return self.get_remote_app_data('internal-port') - - @property - def internal_protocol(self) -> str: - """Return the internal_protocol.""" - return self.get_remote_app_data('internal-protocol') - - @property - def credentials(self) -> str: - return self.get_remote_app_data('credentials') - - @property - def username(self) -> str: - credentials_id = self.get_remote_app_data('credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("username") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def password(self) -> str: - credentials_id = self.get_remote_app_data('credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("password") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def project_name(self) -> str: - """Return the project name.""" - return self.get_remote_app_data('project-name') - - @property - def project_id(self) -> str: - """Return the project id.""" - return self.get_remote_app_data('project-id') - - @property - def user_domain_name(self) -> str: - """Return the name of the user domain.""" - return self.get_remote_app_data('user-domain-name') - - @property - def user_domain_id(self) -> str: - """Return the id of the user domain.""" - return self.get_remote_app_data('user-domain-id') - - @property - def project_domain_name(self) -> str: - """Return the name of the project domain.""" - return self.get_remote_app_data('project-domain-name') - - @property - def project_domain_id(self) -> str: - """Return the id of the project domain.""" - return self.get_remote_app_data('project-domain-id') - - @property - def region(self) -> str: - """Return the region for the auth urls.""" - return self.get_remote_app_data('region') - - def request_credentials(self) -> None: - """Request credentials from the IdentityCredentials server.""" - if self.model.unit.is_leader(): - logging.debug(f'Requesting credentials for {self.charm.app.name}') - app_data = self._identity_credentials_rel.data[self.charm.app] - app_data['username'] = self.charm.app.name - - -class HasIdentityCredentialsClientsEvent(EventBase): - """Has IdentityCredentialsClients Event.""" - - pass - - -class ReadyIdentityCredentialsClientsEvent(EventBase): - """IdentityCredentialsClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, username): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.username = username - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "username": self.username, - } - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.username = snapshot["username"] - - -class IdentityCredentialsClientsGoneAwayEvent(EventBase): - """Has IdentityCredentialsClientsGoneAwayEvent Event.""" - - pass - - -class IdentityCredentialsClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_credentials_clients = EventSource( - HasIdentityCredentialsClientsEvent - ) - ready_identity_credentials_clients = EventSource( - ReadyIdentityCredentialsClientsEvent - ) - identity_credentials_clients_gone = EventSource( - IdentityCredentialsClientsGoneAwayEvent - ) - - -class IdentityCredentialsProvides(Object): - """ - IdentityCredentialsProvides class - """ - - on = IdentityCredentialsClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_credentials_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_credentials_relation_broken, - ) - - def _on_identity_credentials_relation_joined(self, event): - """Handle IdentityCredentials joined.""" - logging.debug("IdentityCredentialsProvides on_joined") - self.on.has_identity_credentials_clients.emit() - - def _on_identity_credentials_relation_changed(self, event): - """Handle IdentityCredentials changed.""" - logging.debug("IdentityCredentials on_changed") - REQUIRED_KEYS = ['username'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - username = event.relation.data[event.relation.app]['username'] - self.on.ready_identity_credentials_clients.emit( - event.relation.id, - event.relation.name, - username, - ) - - def _on_identity_credentials_relation_broken(self, event): - """Handle IdentityCredentials broken.""" - logging.debug("IdentityCredentialsProvides on_departed") - self.on.identity_credentials_clients_gone.emit() - - def set_identity_credentials(self, relation_name: int, - relation_id: str, - api_version: str, - auth_host: str, - auth_port: str, - auth_protocol: str, - internal_host: str, - internal_port: str, - internal_protocol: str, - credentials: str, - project_name: str, - project_id: str, - user_domain_name: str, - user_domain_id: str, - project_domain_name: str, - project_domain_id: str, - region: str): - logging.debug("Setting identity_credentials connection information.") - _identity_credentials_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_credentials_rel = relation - if not _identity_credentials_rel: - # Relation has disappeared so don't send the data - return - app_data = _identity_credentials_rel.data[self.charm.app] - app_data["api-version"] = api_version - app_data["auth-host"] = auth_host - app_data["auth-port"] = str(auth_port) - app_data["auth-protocol"] = auth_protocol - app_data["internal-host"] = internal_host - app_data["internal-port"] = str(internal_port) - app_data["internal-protocol"] = internal_protocol - app_data["credentials"] = credentials - app_data["project-name"] = project_name - app_data["project-id"] = project_id - app_data["user-domain-name"] = user_domain_name - app_data["user-domain-id"] = user_domain_id - app_data["project-domain-name"] = project_domain_name - app_data["project-domain-id"] = project_domain_id - app_data["region"] = region diff --git a/charms/horizon-k8s/lib/charms/keystone_k8s/v1/cloud_credentials.py b/charms/horizon-k8s/lib/charms/keystone_k8s/v1/cloud_credentials.py deleted file mode 100644 index 9ff0a8d3..00000000 --- a/charms/horizon-k8s/lib/charms/keystone_k8s/v1/cloud_credentials.py +++ /dev/null @@ -1,439 +0,0 @@ -"""CloudCredentialsProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the cloud_credentials interface. - -Import `CloudCredentialsRequires` in your charm, with the charm object and the -relation name: - - self - - "cloud_credentials" - -Also provide additional parameters to the charm object: - - service - - internal_url - - public_url - - admin_url - - region - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v0.cloud_credentials import CloudCredentialsRequires - -class CloudCredentialsClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # CloudCredentials Requires - self.cloud_credentials = CloudCredentialsRequires( - self, "cloud_credentials", - service = "my-service" - internal_url = "http://internal-url" - public_url = "http://public-url" - admin_url = "http://admin-url" - region = "region" - ) - self.framework.observe( - self.cloud_credentials.on.connected, self._on_cloud_credentials_connected) - self.framework.observe( - self.cloud_credentials.on.ready, self._on_cloud_credentials_ready) - self.framework.observe( - self.cloud_credentials.on.goneaway, self._on_cloud_credentials_goneaway) - - def _on_cloud_credentials_connected(self, event): - '''React to the CloudCredentials connected event. - - This event happens when n CloudCredentials relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_cloud_credentials_ready(self, event): - '''React to the CloudCredentials ready event. - - The CloudCredentials interface will use the provided config for the - request to the identity server. - ''' - # CloudCredentials Relation is ready. Do something with the completed relation. - pass - - def _on_cloud_credentials_goneaway(self, event): - '''React to the CloudCredentials goneaway event. - - This event happens when an CloudCredentials relation is removed. - ''' - # CloudCredentials Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -# The unique Charmhub library identifier, never change it -LIBID = "a5d96cc2686c47eea554ce2210c2d24e" - -# 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 = 0 - -logger = logging.getLogger(__name__) - - -class CloudCredentialsConnectedEvent(EventBase): - """CloudCredentials connected Event.""" - - pass - - -class CloudCredentialsReadyEvent(EventBase): - """CloudCredentials ready for use Event.""" - - pass - - -class CloudCredentialsGoneAwayEvent(EventBase): - """CloudCredentials relation has gone-away Event""" - - pass - - -class CloudCredentialsServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(CloudCredentialsConnectedEvent) - ready = EventSource(CloudCredentialsReadyEvent) - goneaway = EventSource(CloudCredentialsGoneAwayEvent) - - -class CloudCredentialsRequires(Object): - """ - CloudCredentialsRequires class - """ - - on = CloudCredentialsServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_cloud_credentials_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_cloud_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_cloud_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_cloud_credentials_relation_broken, - ) - - def _on_cloud_credentials_relation_joined(self, event): - """CloudCredentials relation joined.""" - logging.debug("CloudCredentials on_joined") - self.on.connected.emit() - self.request_credentials() - - def _on_cloud_credentials_relation_changed(self, event): - """CloudCredentials relation changed.""" - logging.debug("CloudCredentials on_changed") - try: - self.on.ready.emit() - except (AttributeError, KeyError): - logger.exception('Error when emitting event') - - def _on_cloud_credentials_relation_broken(self, event): - """CloudCredentials relation broken.""" - logging.debug("CloudCredentials on_broken") - self.on.goneaway.emit() - - @property - def _cloud_credentials_rel(self) -> Relation: - """The CloudCredentials relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._cloud_credentials_rel.data[self._cloud_credentials_rel.app] - return data.get(key) - - @property - def api_version(self) -> str: - """Return the api_version.""" - return self.get_remote_app_data('api-version') - - @property - def auth_host(self) -> str: - """Return the auth_host.""" - return self.get_remote_app_data('auth-host') - - @property - def auth_port(self) -> str: - """Return the auth_port.""" - return self.get_remote_app_data('auth-port') - - @property - def auth_protocol(self) -> str: - """Return the auth_protocol.""" - return self.get_remote_app_data('auth-protocol') - - @property - def internal_host(self) -> str: - """Return the internal_host.""" - return self.get_remote_app_data('internal-host') - - @property - def internal_port(self) -> str: - """Return the internal_port.""" - return self.get_remote_app_data('internal-port') - - @property - def internal_protocol(self) -> str: - """Return the internal_protocol.""" - return self.get_remote_app_data('internal-protocol') - - @property - def credentials(self) -> str: - return self.get_remote_app_data('credentials') - - @property - def username(self) -> str: - credentials_id = self.get_remote_app_data('credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("username") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def password(self) -> str: - credentials_id = self.get_remote_app_data('credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("password") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def project_name(self) -> str: - """Return the project name.""" - return self.get_remote_app_data('project-name') - - @property - def project_id(self) -> str: - """Return the project id.""" - return self.get_remote_app_data('project-id') - - @property - def user_domain_name(self) -> str: - """Return the name of the user domain.""" - return self.get_remote_app_data('user-domain-name') - - @property - def user_domain_id(self) -> str: - """Return the id of the user domain.""" - return self.get_remote_app_data('user-domain-id') - - @property - def project_domain_name(self) -> str: - """Return the name of the project domain.""" - return self.get_remote_app_data('project-domain-name') - - @property - def project_domain_id(self) -> str: - """Return the id of the project domain.""" - return self.get_remote_app_data('project-domain-id') - - @property - def region(self) -> str: - """Return the region for the auth urls.""" - return self.get_remote_app_data('region') - - def request_credentials(self) -> None: - """Request credentials from the CloudCredentials server.""" - if self.model.unit.is_leader(): - logging.debug(f'Requesting credentials for {self.charm.app.name}') - app_data = self._cloud_credentials_rel.data[self.charm.app] - app_data['username'] = self.charm.app.name - - -class HasCloudCredentialsClientsEvent(EventBase): - """Has CloudCredentialsClients Event.""" - - pass - - -class ReadyCloudCredentialsClientsEvent(EventBase): - """CloudCredentialsClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, username): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.username = username - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "username": self.username, - } - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.username = snapshot["username"] - - -class CloudCredentialsClientsGoneAwayEvent(EventBase): - """Has CloudCredentialsClientsGoneAwayEvent Event.""" - - pass - - -class CloudCredentialsClientEvents(ObjectEvents): - """Events class for `on`""" - - has_cloud_credentials_clients = EventSource( - HasCloudCredentialsClientsEvent - ) - ready_cloud_credentials_clients = EventSource( - ReadyCloudCredentialsClientsEvent - ) - cloud_credentials_clients_gone = EventSource( - CloudCredentialsClientsGoneAwayEvent - ) - - -class CloudCredentialsProvides(Object): - """ - CloudCredentialsProvides class - """ - - on = CloudCredentialsClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_cloud_credentials_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_cloud_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_cloud_credentials_relation_broken, - ) - - def _on_cloud_credentials_relation_joined(self, event): - """Handle CloudCredentials joined.""" - logging.debug("CloudCredentialsProvides on_joined") - self.on.has_cloud_credentials_clients.emit() - - def _on_cloud_credentials_relation_changed(self, event): - """Handle CloudCredentials changed.""" - logging.debug("CloudCredentials on_changed") - REQUIRED_KEYS = ['username'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - username = event.relation.data[event.relation.app]['username'] - self.on.ready_cloud_credentials_clients.emit( - event.relation.id, - event.relation.name, - username, - ) - - def _on_cloud_credentials_relation_broken(self, event): - """Handle CloudCredentials broken.""" - logging.debug("CloudCredentialsProvides on_departed") - self.on.cloud_credentials_clients_gone.emit() - - def set_cloud_credentials(self, relation_name: int, - relation_id: str, - api_version: str, - auth_host: str, - auth_port: str, - auth_protocol: str, - internal_host: str, - internal_port: str, - internal_protocol: str, - credentials: str, - project_name: str, - project_id: str, - user_domain_name: str, - user_domain_id: str, - project_domain_name: str, - project_domain_id: str, - region: str): - logging.debug("Setting cloud_credentials connection information.") - _cloud_credentials_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _cloud_credentials_rel = relation - if not _cloud_credentials_rel: - # Relation has disappeared so don't send the data - return - app_data = _cloud_credentials_rel.data[self.charm.app] - app_data["api-version"] = api_version - app_data["auth-host"] = auth_host - app_data["auth-port"] = str(auth_port) - app_data["auth-protocol"] = auth_protocol - app_data["internal-host"] = internal_host - app_data["internal-port"] = str(internal_port) - app_data["internal-protocol"] = internal_protocol - app_data["credentials"] = credentials - app_data["project-name"] = project_name - app_data["project-id"] = project_id - app_data["user-domain-name"] = user_domain_name - app_data["user-domain-id"] = user_domain_id - app_data["project-domain-name"] = project_domain_name - app_data["project-domain-id"] = project_domain_id - app_data["region"] = region diff --git a/charms/horizon-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/horizon-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 35556622..00000000 --- a/charms/horizon-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,518 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -Also provide additional parameters to the charm object: - - service - - internal_url - - public_url - - admin_url - - region - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - service = "my-service" - internal_url = "http://internal-url" - public_url = "http://public-url" - admin_url = "http://admin-url" - region = "region" - ) - self.framework.observe( - self.identity_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# 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 = 0 - - -logger = logging.getLogger(__name__) - - -class IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._identity_service_rel.data[self._identity_service_rel.app] - return data.get(key) - - @property - def api_version(self) -> str: - """Return the api_version.""" - return self.get_remote_app_data('api-version') - - @property - def auth_host(self) -> str: - """Return the auth_host.""" - return self.get_remote_app_data('auth-host') - - @property - def auth_port(self) -> str: - """Return the auth_port.""" - return self.get_remote_app_data('auth-port') - - @property - def auth_protocol(self) -> str: - """Return the auth_protocol.""" - return self.get_remote_app_data('auth-protocol') - - @property - def internal_host(self) -> str: - """Return the internal_host.""" - return self.get_remote_app_data('internal-host') - - @property - def internal_port(self) -> str: - """Return the internal_port.""" - return self.get_remote_app_data('internal-port') - - @property - def internal_protocol(self) -> str: - """Return the internal_protocol.""" - return self.get_remote_app_data('internal-protocol') - - @property - def admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("password") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("username") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_credentials(self, relation_name: int, - relation_id: str, - api_version: str, - auth_host: str, - auth_port: str, - auth_protocol: str, - internal_host: str, - internal_port: str, - internal_protocol: str, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_rel.data[self.charm.app] - app_data["api-version"] = api_version - app_data["auth-host"] = auth_host - app_data["auth-port"] = str(auth_port) - app_data["auth-protocol"] = auth_protocol - app_data["internal-host"] = internal_host - app_data["internal-port"] = str(internal_port) - app_data["internal-protocol"] = internal_protocol - app_data["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials diff --git a/charms/horizon-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py b/charms/horizon-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py deleted file mode 100644 index c3fac4ca..00000000 --- a/charms/horizon-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py +++ /dev/null @@ -1,227 +0,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/charms/horizon-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/horizon-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/horizon-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/horizon-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/horizon-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/horizon-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/horizon-k8s/osci.yaml b/charms/horizon-k8s/osci.yaml deleted file mode 100644 index 3e23b3e6..00000000 --- a/charms/horizon-k8s/osci.yaml +++ /dev/null @@ -1,12 +0,0 @@ -- project: - templates: - - charm-unit-jobs-py38 - - charm-unit-jobs-py310 - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: horizon-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/horizon-k8s/pyproject.toml b/charms/horizon-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/horizon-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/horizon-k8s/rename.sh b/charms/horizon-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/horizon-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/horizon-k8s/requirements.txt b/charms/horizon-k8s/requirements.txt index 11632a5d..64399ba9 100644 --- a/charms/horizon-k8s/requirements.txt +++ b/charms/horizon-k8s/requirements.txt @@ -11,4 +11,6 @@ pydantic<2.0 lightkube lightkube-models ops -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam + +# From ops_sunbeam +tenacity diff --git a/charms/horizon-k8s/src/templates/parts/database-connection b/charms/horizon-k8s/src/templates/parts/database-connection deleted file mode 100644 index 1fd70ce2..00000000 --- a/charms/horizon-k8s/src/templates/parts/database-connection +++ /dev/null @@ -1,3 +0,0 @@ -{% if database.connection -%} -connection = {{ database.connection }} -{% endif -%} diff --git a/charms/horizon-k8s/test-requirements.txt b/charms/horizon-k8s/test-requirements.txt deleted file mode 100644 index ebee5935..00000000 --- a/charms/horizon-k8s/test-requirements.txt +++ /dev/null @@ -1,12 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -coverage -mock -flake8 -stestr -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/horizon-k8s/tests/unit/test_horizon_charm.py b/charms/horizon-k8s/tests/unit/test_horizon_charm.py index 48be6733..95a041a3 100644 --- a/charms/horizon-k8s/tests/unit/test_horizon_charm.py +++ b/charms/horizon-k8s/tests/unit/test_horizon_charm.py @@ -16,11 +16,10 @@ """Unit tests for Horizon operator.""" +import charm import mock import ops_sunbeam.test_utils as test_utils -import charm - class _HorizonOperatorCharm(charm.HorizonOperatorCharm): """Test Operator Charm for Horizon Operator.""" diff --git a/charms/horizon-k8s/tox.ini b/charms/horizon-k8s/tox.ini deleted file mode 100644 index 671cda90..00000000 --- a/charms/horizon-k8s/tox.ini +++ /dev/null @@ -1,132 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/keystone-k8s/.flake8 b/charms/keystone-k8s/.flake8 deleted file mode 100644 index c0a92a06..00000000 --- a/charms/keystone-k8s/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 80 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/charms/keystone-k8s/.gitignore b/charms/keystone-k8s/.gitignore deleted file mode 100644 index 4df34f6a..00000000 --- a/charms/keystone-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -.idea/ -*.charm -.tox -venv -.coverage -__pycache__/ -*.py[cod] -**.swp -.stestr/ diff --git a/charms/keystone-k8s/.gitreview b/charms/keystone-k8s/.gitreview deleted file mode 100644 index ba705e1e..00000000 --- a/charms/keystone-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-keystone-k8s.git -defaultbranch=main diff --git a/charms/keystone-k8s/.stestr.conf b/charms/keystone-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/keystone-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/keystone-k8s/.zuul.yaml b/charms/keystone-k8s/.zuul.yaml deleted file mode 100644 index bfda2e24..00000000 --- a/charms/keystone-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: keystone-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/keystone-k8s/charmcraft.yaml b/charms/keystone-k8s/charmcraft.yaml index 0c6b3bdb..cbd210b0 100644 --- a/charms/keystone-k8s/charmcraft.yaml +++ b/charms/keystone-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/keystone-k8s/fetch-libs.sh b/charms/keystone-k8s/fetch-libs.sh deleted file mode 100755 index a49af7ed..00000000 --- a/charms/keystone-k8s/fetch-libs.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.nginx_ingress_integrator.v0.ingress -charmcraft fetch-lib charms.data_platform_libs.v0.database_requires -#charmcraft fetch-lib charms.sunbeam_keystone_operator.v1.identity_service -#charmcraft fetch-lib charms.sunbeam_keystone_operator.v0.identity_credentials -charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.traefik_k8s.v2.ingress diff --git a/charms/keystone-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/keystone-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 53d61912..00000000 --- a/charms/keystone-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,496 +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. - -"""Relation 'requires' side abstraction for database relation. - -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.database_requires import 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.database_requires import 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, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(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 password(self) -> Optional[str]: - """Returns the password for the created user.""" - return self.relation.data[self.relation.app].get("password") - - @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 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") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - return self.relation.data[self.relation.app].get("username") - - @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(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(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) - - -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 — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() - - 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) - self.charm = charm - self.database = database_name - 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.relations_aliases = relations_aliases - 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 - ) - - # 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 _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. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].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] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - 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 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) - - 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") - - @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]) diff --git a/charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py b/charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py deleted file mode 100644 index e3f4565d..00000000 --- a/charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py +++ /dev/null @@ -1,458 +0,0 @@ -"""IdentityCredentialsProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_credentials interface. - -Import `IdentityCredentialsRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_credentials" - -Also provide additional parameters to the charm object: - - service - - internal_url - - public_url - - admin_url - - region - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v0.identity_credentials import IdentityCredentialsRequires - -class IdentityCredentialsClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityCredentials Requires - self.identity_credentials = IdentityCredentialsRequires( - self, "identity_credentials", - service = "my-service" - internal_url = "http://internal-url" - public_url = "http://public-url" - admin_url = "http://admin-url" - region = "region" - ) - self.framework.observe( - self.identity_credentials.on.connected, self._on_identity_credentials_connected) - self.framework.observe( - self.identity_credentials.on.ready, self._on_identity_credentials_ready) - self.framework.observe( - self.identity_credentials.on.goneaway, self._on_identity_credentials_goneaway) - - def _on_identity_credentials_connected(self, event): - '''React to the IdentityCredentials connected event. - - This event happens when IdentityCredentials relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_credentials_ready(self, event): - '''React to the IdentityCredentials ready event. - - The IdentityCredentials interface will use the provided config for the - request to the identity server. - ''' - # IdentityCredentials Relation is ready. Do something with the completed relation. - pass - - def _on_identity_credentials_goneaway(self, event): - '''React to the IdentityCredentials goneaway event. - - This event happens when an IdentityCredentials relation is removed. - ''' - # IdentityCredentials Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -# The unique Charmhub library identifier, never change it -LIBID = "b5fa18d4427c4ab9a269c3a2fbed545c" - -# 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 IdentityCredentialsConnectedEvent(EventBase): - """IdentityCredentials connected Event.""" - - pass - - -class IdentityCredentialsReadyEvent(EventBase): - """IdentityCredentials ready for use Event.""" - - pass - - -class IdentityCredentialsGoneAwayEvent(EventBase): - """IdentityCredentials relation has gone-away Event""" - - pass - - -class IdentityCredentialsServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityCredentialsConnectedEvent) - ready = EventSource(IdentityCredentialsReadyEvent) - goneaway = EventSource(IdentityCredentialsGoneAwayEvent) - - -class IdentityCredentialsRequires(Object): - """ - IdentityCredentialsRequires class - """ - - on = IdentityCredentialsServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_credentials_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_credentials_relation_broken, - ) - - def _on_identity_credentials_relation_joined(self, event): - """IdentityCredentials relation joined.""" - logging.debug("IdentityCredentials on_joined") - self.on.connected.emit() - self.request_credentials() - - def _on_identity_credentials_relation_changed(self, event): - """IdentityCredentials relation changed.""" - logging.debug("IdentityCredentials on_changed") - try: - self.on.ready.emit() - except (AttributeError, KeyError): - logger.exception('Error when emitting event') - - def _on_identity_credentials_relation_broken(self, event): - """IdentityCredentials relation broken.""" - logging.debug("IdentityCredentials on_broken") - self.on.goneaway.emit() - - @property - def _identity_credentials_rel(self) -> Relation: - """The IdentityCredentials relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._identity_credentials_rel.data[self._identity_credentials_rel.app] - return data.get(key) - - @property - def api_version(self) -> str: - """Return the api_version.""" - return self.get_remote_app_data('api-version') - - @property - def auth_host(self) -> str: - """Return the auth_host.""" - return self.get_remote_app_data('auth-host') - - @property - def auth_port(self) -> str: - """Return the auth_port.""" - return self.get_remote_app_data('auth-port') - - @property - def auth_protocol(self) -> str: - """Return the auth_protocol.""" - return self.get_remote_app_data('auth-protocol') - - @property - def internal_host(self) -> str: - """Return the internal_host.""" - return self.get_remote_app_data('internal-host') - - @property - def internal_port(self) -> str: - """Return the internal_port.""" - return self.get_remote_app_data('internal-port') - - @property - def internal_protocol(self) -> str: - """Return the internal_protocol.""" - return self.get_remote_app_data('internal-protocol') - - @property - def credentials(self) -> str: - return self.get_remote_app_data('credentials') - - @property - def username(self) -> str: - credentials_id = self.get_remote_app_data('credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("username") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def password(self) -> str: - credentials_id = self.get_remote_app_data('credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("password") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def project_name(self) -> str: - """Return the project name.""" - return self.get_remote_app_data('project-name') - - @property - def project_id(self) -> str: - """Return the project id.""" - return self.get_remote_app_data('project-id') - - @property - def user_domain_name(self) -> str: - """Return the name of the user domain.""" - return self.get_remote_app_data('user-domain-name') - - @property - def user_domain_id(self) -> str: - """Return the id of the user domain.""" - return self.get_remote_app_data('user-domain-id') - - @property - def project_domain_name(self) -> str: - """Return the name of the project domain.""" - return self.get_remote_app_data('project-domain-name') - - @property - def project_domain_id(self) -> str: - """Return the id of the project domain.""" - return self.get_remote_app_data('project-domain-id') - - @property - def region(self) -> str: - """Return the region for the auth urls.""" - return self.get_remote_app_data('region') - - @property - def internal_endpoint(self) -> str: - """Return the region for the internal auth url.""" - return self.get_remote_app_data('internal-endpoint') - - @property - def public_endpoint(self) -> str: - """Return the region for the public auth url.""" - return self.get_remote_app_data('public-endpoint') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def request_credentials(self) -> None: - """Request credentials from the IdentityCredentials server.""" - if self.model.unit.is_leader(): - logging.debug(f'Requesting credentials for {self.charm.app.name}') - app_data = self._identity_credentials_rel.data[self.charm.app] - app_data['username'] = self.charm.app.name - - -class HasIdentityCredentialsClientsEvent(EventBase): - """Has IdentityCredentialsClients Event.""" - - pass - - -class ReadyIdentityCredentialsClientsEvent(EventBase): - """IdentityCredentialsClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, username): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.username = username - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "username": self.username, - } - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.username = snapshot["username"] - - -class IdentityCredentialsClientsGoneAwayEvent(EventBase): - """Has IdentityCredentialsClientsGoneAwayEvent Event.""" - - pass - - -class IdentityCredentialsClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_credentials_clients = EventSource( - HasIdentityCredentialsClientsEvent - ) - ready_identity_credentials_clients = EventSource( - ReadyIdentityCredentialsClientsEvent - ) - identity_credentials_clients_gone = EventSource( - IdentityCredentialsClientsGoneAwayEvent - ) - - -class IdentityCredentialsProvides(Object): - """ - IdentityCredentialsProvides class - """ - - on = IdentityCredentialsClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_credentials_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_credentials_relation_broken, - ) - - def _on_identity_credentials_relation_joined(self, event): - """Handle IdentityCredentials joined.""" - logging.debug("IdentityCredentialsProvides on_joined") - self.on.has_identity_credentials_clients.emit() - - def _on_identity_credentials_relation_changed(self, event): - """Handle IdentityCredentials changed.""" - logging.debug("IdentityCredentials on_changed") - REQUIRED_KEYS = ['username'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - username = event.relation.data[event.relation.app]['username'] - self.on.ready_identity_credentials_clients.emit( - event.relation.id, - event.relation.name, - username, - ) - - def _on_identity_credentials_relation_broken(self, event): - """Handle IdentityCredentials broken.""" - logging.debug("IdentityCredentialsProvides on_departed") - self.on.identity_credentials_clients_gone.emit() - - def set_identity_credentials(self, relation_name: int, - relation_id: str, - api_version: str, - auth_host: str, - auth_port: str, - auth_protocol: str, - internal_host: str, - internal_port: str, - internal_protocol: str, - credentials: str, - project_name: str, - project_id: str, - user_domain_name: str, - user_domain_id: str, - project_domain_name: str, - project_domain_id: str, - region: str, - admin_role: str): - logging.debug("Setting identity_credentials connection information.") - _identity_credentials_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_credentials_rel = relation - if not _identity_credentials_rel: - # Relation has disappeared so don't send the data - return - app_data = _identity_credentials_rel.data[self.charm.app] - app_data["api-version"] = api_version - app_data["auth-host"] = auth_host - app_data["auth-port"] = str(auth_port) - app_data["auth-protocol"] = auth_protocol - app_data["internal-host"] = internal_host - app_data["internal-port"] = str(internal_port) - app_data["internal-protocol"] = internal_protocol - app_data["credentials"] = credentials - app_data["project-name"] = project_name - app_data["project-id"] = project_id - app_data["user-domain-name"] = user_domain_name - app_data["user-domain-id"] = user_domain_id - app_data["project-domain-name"] = project_domain_name - app_data["project-domain-id"] = project_domain_id - app_data["region"] = region - app_data["internal-endpoint"] = self.charm.internal_endpoint - app_data["public-endpoint"] = self.charm.public_endpoint - app_data["admin-role"] = admin_role diff --git a/charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_resource.py b/charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_resource.py deleted file mode 100644 index 1f10383a..00000000 --- a/charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_resource.py +++ /dev/null @@ -1,393 +0,0 @@ -"""IdentityResourceProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the identity_ops interface. - -Import `IdentityResourceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_ops" - -Also provide additional parameters to the charm object: - - request - -Three events are also available to respond to: - - provider_ready - - provider_goneaway - - response_avaialable - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v0.identity_resource import IdentityResourceRequires - -class IdentityResourceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityResource Requires - self.identity_resource = IdentityResourceRequires( - self, "identity_ops", - ) - self.framework.observe( - self.identity_resource.on.provider_ready, self._on_identity_resource_ready) - self.framework.observe( - self.identity_resource.on.provider_goneaway, self._on_identity_resource_goneaway) - self.framework.observe( - self.identity_resource.on.response_available, self._on_identity_resource_response) - - def _on_identity_resource_ready(self, event): - '''React to the IdentityResource provider_ready event. - - This event happens when n IdentityResource relation is added to the - model. Ready to send any ops to keystone. - ''' - # Ready to send any ops. - pass - - def _on_identity_resource_response(self, event): - '''React to the IdentityResource response_available event. - - The IdentityResource interface will provide the response for the ops sent. - ''' - # Read the response for the ops sent. - pass - - def _on_identity_resource_goneaway(self, event): - '''React to the IdentityResource goneaway event. - - This event happens when an IdentityResource relation is removed. - ''' - # IdentityResource Relation has goneaway. No ops can be sent. - pass -``` - -A sample ops request can be of format -{ - "id": - "tag": - "ops": [ - { - "name": , - "params": { - : , - : - } - } - ] -} - -For any sensitive data in the ops params, the charm can create secrets and pass -secret id instead of sensitive data as part of ops request. The charm should -ensure to grant secret access to provider charm i.e., keystone over relation. -The secret content should hold the sensitive data with same name as param name. -""" - -import json -import logging -from typing import ( - Optional, -) - -from ops.charm import ( - CharmBase, - RelationBrokenEvent, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import ( - EventBase, - EventSource, - Object, - ObjectEvents, - StoredState, -) -from ops.model import ( - Relation, -) - -logger = logging.getLogger(__name__) - - -# The unique Charmhub library identifier, never change it -LIBID = "b419d4d8249e423487daafc3665ed06f" - -# 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 - - -REQUEST_NOT_SENT = 1 -REQUEST_SENT = 2 -REQUEST_PROCESSED = 3 - - -class IdentityOpsProviderReadyEvent(RelationEvent): - """Has IdentityOpsProviderReady Event.""" - - pass - - -class IdentityOpsResponseEvent(RelationEvent): - """Has IdentityOpsResponse Event.""" - - pass - - -class IdentityOpsProviderGoneAwayEvent(RelationEvent): - """Has IdentityOpsProviderGoneAway Event.""" - - pass - - -class IdentityResourceResponseEvents(ObjectEvents): - """Events class for `on`.""" - - provider_ready = EventSource(IdentityOpsProviderReadyEvent) - response_available = EventSource(IdentityOpsResponseEvent) - provider_goneaway = EventSource(IdentityOpsProviderGoneAwayEvent) - - -class IdentityResourceRequires(Object): - """IdentityResourceRequires class.""" - - on = IdentityResourceResponseEvents() - _stored = StoredState() - - def __init__(self, charm: CharmBase, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self._stored.set_default(provider_ready=False, requests=[]) - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_resource_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_resource_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_resource_relation_broken, - ) - - def _on_identity_resource_relation_joined( - self, event: RelationJoinedEvent - ): - """Handle IdentityResource joined.""" - self._stored.provider_ready = True - self.on.provider_ready.emit(event.relation) - - def _on_identity_resource_relation_changed( - self, event: RelationChangedEvent - ): - """Handle IdentityResource changed.""" - id_ = self.response.get("id") - self.save_request_in_store(id_, None, None, REQUEST_PROCESSED) - self.on.response_available.emit(event.relation) - - def _on_identity_resource_relation_broken( - self, event: RelationBrokenEvent - ): - """Handle IdentityResource broken.""" - self._stored.provider_ready = False - self.on.provider_goneaway.emit(event.relation) - - @property - def _identity_resource_rel(self) -> Optional[Relation]: - """The IdentityResource relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def response(self) -> dict: - """Response object from keystone.""" - response = self.get_remote_app_data("response") - if not response: - return {} - - try: - return json.loads(response) - except Exception as e: - logger.debug(str(e)) - - return {} - - def save_request_in_store( - self, id: str, tag: str, ops: list, state: int - ) -> None: - """Save request in the store.""" - if id is None: - return - - for request in self._stored.requests: - if request.get("id") == id: - if tag: - request["tag"] = tag - if ops: - request["ops"] = ops - request["state"] = state - return - - # New request - self._stored.requests.append( - {"id": id, "tag": tag, "ops": ops, "state": state} - ) - - def get_request_from_store(self, id: str) -> dict: - """Get request from the stote.""" - for request in self._stored.requests: - if request.get("id") == id: - return request - - return {} - - def is_request_processed(self, id: str) -> bool: - """Check if request is processed.""" - for request in self._stored.requests: - if ( - request.get("id") == id - and request.get("state") == REQUEST_PROCESSED - ): - return True - - return False - - def get_remote_app_data(self, key: str) -> Optional[str]: - """Return the value for the given key from remote app data.""" - if self._identity_resource_rel: - data = self._identity_resource_rel.data[ - self._identity_resource_rel.app - ] - return data.get(key) - - return None - - def ready(self) -> bool: - """Interface is ready or not. - - Interface is considered ready if the op request is processed - and response is sent. In case of non leader unit, just consider - the interface is ready. - """ - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, set the interface to ready") - return True - - try: - app_data = self._identity_resource_rel.data[self.charm.app] - if "request" not in app_data: - return False - - request = json.loads(app_data["request"]) - request_id = request.get("id") - response_id = self.response.get("id") - if request_id == response_id: - return True - except Exception as e: - logger.debug(str(e)) - - return False - - def request_ops(self, request: dict) -> None: - """Request keystone ops.""" - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, not sending request") - return - - id_ = request.get("id") - tag = request.get("tag") - ops = request.get("ops") - req = self.get_request_from_store(id_) - if req and req.get("state") == REQUEST_PROCESSED: - logger.debug("Request {id_} already processed") - return - - if not self._stored.provider_ready: - self.save_request_in_store(id_, tag, ops, REQUEST_NOT_SENT) - logger.debug("Keystone not yet ready to take requests") - return - - logger.debug("Requesting ops to keystone") - app_data = self._identity_resource_rel.data[self.charm.app] - app_data["request"] = json.dumps(request) - self.save_request_in_store(id_, tag, ops, REQUEST_SENT) - - -class IdentityOpsRequestEvent(EventBase): - """Has IdentityOpsRequest Event.""" - - def __init__(self, handle, relation_id, relation_name, request): - """Initialise event.""" - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.request = request - - def snapshot(self): - """Snapshot the event.""" - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "request": self.request, - } - - def restore(self, snapshot): - """Restore the event.""" - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.request = snapshot["request"] - - -class IdentityResourceProviderEvents(ObjectEvents): - """Events class for `on`.""" - - process_op = EventSource(IdentityOpsRequestEvent) - - -class IdentityResourceProvides(Object): - """IdentityResourceProvides class.""" - - on = IdentityResourceProviderEvents() - - def __init__(self, charm: CharmBase, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_resource_relation_changed, - ) - - def _on_identity_resource_relation_changed( - self, event: RelationChangedEvent - ): - """Handle IdentityResource changed.""" - request = event.relation.data[event.relation.app].get("request") - if request is not None: - self.on.process_op.emit( - event.relation.id, event.relation.name, request - ) - - def set_ops_response( - self, relation_id: str, relation_name: str, ops_response: dict - ) -> None: - """Set response to ops request.""" - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, not sending response") - return - - logger.debug("Update response from keystone") - _identity_resource_rel = self.charm.model.get_relation( - relation_name, relation_id - ) - if not _identity_resource_rel: - # Relation has disappeared so skip send of data - return - - app_data = _identity_resource_rel.data[self.charm.app] - app_data["response"] = json.dumps(ops_response) diff --git a/charms/keystone-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py b/charms/keystone-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py deleted file mode 100644 index c3fac4ca..00000000 --- a/charms/keystone-k8s/lib/charms/nginx_ingress_integrator/v0/ingress.py +++ /dev/null @@ -1,227 +0,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/charms/keystone-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/keystone-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/keystone-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/keystone-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/keystone-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/keystone-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/keystone-k8s/osci.yaml b/charms/keystone-k8s/osci.yaml deleted file mode 100644 index 0d6d4c1c..00000000 --- a/charms/keystone-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: keystone-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/keystone-k8s/pyproject.toml b/charms/keystone-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/keystone-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/keystone-k8s/rename.sh b/charms/keystone-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/keystone-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/keystone-k8s/requirements.txt b/charms/keystone-k8s/requirements.txt index 1bec875c..e9b413e7 100644 --- a/charms/keystone-k8s/requirements.txt +++ b/charms/keystone-k8s/requirements.txt @@ -13,6 +13,7 @@ lightkube-models ops pwgen -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam - python-keystoneclient # keystone-k8s + +# From ops_sunbeam +tenacity diff --git a/charms/keystone-k8s/src/charm.py b/charms/keystone-k8s/src/charm.py index 2f0d0628..9a65ddc2 100755 --- a/charms/keystone-k8s/src/charm.py +++ b/charms/keystone-k8s/src/charm.py @@ -71,7 +71,6 @@ from ops.model import ( SecretNotFoundError, SecretRotate, ) - from utils import ( manager, ) diff --git a/charms/keystone-k8s/src/templates/parts/section-database b/charms/keystone-k8s/src/templates/parts/section-database deleted file mode 100644 index 88b0abd5..00000000 --- a/charms/keystone-k8s/src/templates/parts/section-database +++ /dev/null @@ -1,7 +0,0 @@ -[database] -{% if database.connection -%} -connection = {{ database.connection }} -{% else -%} -connection = sqlite:////var/lib/keystone/keystone.db -{% endif -%} -connection_recycle_time = 200 diff --git a/charms/keystone-k8s/src/templates/parts/section-federation b/charms/keystone-k8s/src/templates/parts/section-federation deleted file mode 100644 index 65ee99ed..00000000 --- a/charms/keystone-k8s/src/templates/parts/section-federation +++ /dev/null @@ -1,10 +0,0 @@ -{% if trusted_dashboards %} -[federation] -{% for dashboard_url in trusted_dashboards -%} -trusted_dashboard = {{ dashboard_url }} -{% endfor -%} -{% endif %} -{% for sp in fid_sps -%} -[{{ sp['protocol-name'] }}] -remote_id_attribute = {{ sp['remote-id-attribute'] }} -{% endfor -%} diff --git a/charms/keystone-k8s/src/templates/parts/section-middleware b/charms/keystone-k8s/src/templates/parts/section-middleware deleted file mode 100644 index e65f1d98..00000000 --- a/charms/keystone-k8s/src/templates/parts/section-middleware +++ /dev/null @@ -1,6 +0,0 @@ -{% for section in sections -%} -[{{section}}] -{% for key, value in sections[section].items() -%} -{{ key }} = {{ value }} -{% endfor %} -{%- endfor %} diff --git a/charms/keystone-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/keystone-k8s/src/templates/parts/section-oslo-messaging-rabbit deleted file mode 100644 index 145c4ee9..00000000 --- a/charms/keystone-k8s/src/templates/parts/section-oslo-messaging-rabbit +++ /dev/null @@ -1,2 +0,0 @@ -[oslo_messaging_rabbit] -rabbit_quorum_queue = True diff --git a/charms/keystone-k8s/src/templates/parts/section-oslo-notifications b/charms/keystone-k8s/src/templates/parts/section-oslo-notifications deleted file mode 100644 index ce559feb..00000000 --- a/charms/keystone-k8s/src/templates/parts/section-oslo-notifications +++ /dev/null @@ -1,4 +0,0 @@ -{% if options.enable_telemetry_notifications -%} -[oslo_messaging_notifications] -driver = messagingv2 -{%- endif %} diff --git a/charms/keystone-k8s/src/templates/parts/section-signing b/charms/keystone-k8s/src/templates/parts/section-signing deleted file mode 100644 index cb7d69ae..00000000 --- a/charms/keystone-k8s/src/templates/parts/section-signing +++ /dev/null @@ -1,15 +0,0 @@ -{% if enable_signing -%} -[signing] -{% if certfile -%} -certfile = {{ certfile }} -{% endif -%} -{% if keyfile -%} -keyfile = {{ keyfile }} -{% endif -%} -{% if ca_certs -%} -ca_certs = {{ ca_certs }} -{% endif -%} -{% if ca_key -%} -ca_key = {{ ca_key }} -{% endif -%} -{% endif -%} \ No newline at end of file diff --git a/charms/keystone-k8s/src/utils/manager.py b/charms/keystone-k8s/src/utils/manager.py index 182ae147..91d282f0 100644 --- a/charms/keystone-k8s/src/utils/manager.py +++ b/charms/keystone-k8s/src/utils/manager.py @@ -37,7 +37,6 @@ from ops import ( from ops.model import ( MaintenanceStatus, ) - from utils.client import ( KeystoneClient, KeystoneExceptionError, diff --git a/charms/keystone-k8s/test-requirements.txt b/charms/keystone-k8s/test-requirements.txt deleted file mode 100644 index 23e005bd..00000000 --- a/charms/keystone-k8s/test-requirements.txt +++ /dev/null @@ -1,17 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -pwgen -coverage -mock -flake8 -stestr -git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza -git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack -git+https://opendev.org/openstack/tempest.git#egg=tempest -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/keystone-k8s/tests/unit/test_keystone_charm.py b/charms/keystone-k8s/tests/unit/test_keystone_charm.py index 37af2812..96c76ebb 100644 --- a/charms/keystone-k8s/tests/unit/test_keystone_charm.py +++ b/charms/keystone-k8s/tests/unit/test_keystone_charm.py @@ -24,11 +24,10 @@ from unittest.mock import ( MagicMock, ) +import charm import mock import ops_sunbeam.test_utils as test_utils -import charm - class _KeystoneOperatorCharm(charm.KeystoneOperatorCharm): """Create Keystone operator test charm.""" diff --git a/charms/keystone-k8s/tox.ini b/charms/keystone-k8s/tox.ini deleted file mode 100644 index 57750258..00000000 --- a/charms/keystone-k8s/tox.ini +++ /dev/null @@ -1,161 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - PYTHONPATH - HOME -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true;default-series= - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/keystone-ldap-k8s/.gitignore b/charms/keystone-ldap-k8s/.gitignore deleted file mode 100644 index 4df34f6a..00000000 --- a/charms/keystone-ldap-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -.idea/ -*.charm -.tox -venv -.coverage -__pycache__/ -*.py[cod] -**.swp -.stestr/ diff --git a/charms/keystone-ldap-k8s/.gitreview b/charms/keystone-ldap-k8s/.gitreview deleted file mode 100644 index 069407b6..00000000 --- a/charms/keystone-ldap-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-keystone-ldap-k8s.git -defaultbranch=main diff --git a/charms/keystone-ldap-k8s/.stestr.conf b/charms/keystone-ldap-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/keystone-ldap-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/keystone-ldap-k8s/.zuul.yaml b/charms/keystone-ldap-k8s/.zuul.yaml deleted file mode 100644 index 17366a2c..00000000 --- a/charms/keystone-ldap-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: keystone-ldap-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/keystone-ldap-k8s/charmcraft.yaml b/charms/keystone-ldap-k8s/charmcraft.yaml index 8c3cfc6f..12fc2d53 100644 --- a/charms/keystone-ldap-k8s/charmcraft.yaml +++ b/charms/keystone-ldap-k8s/charmcraft.yaml @@ -27,4 +27,3 @@ parts: - cryptography - jsonschema - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/keystone-ldap-k8s/lib/charms/keystone_k8s/v0/domain_config.py b/charms/keystone-ldap-k8s/lib/charms/keystone_k8s/v0/domain_config.py deleted file mode 100644 index 0d9858c9..00000000 --- a/charms/keystone-ldap-k8s/lib/charms/keystone_k8s/v0/domain_config.py +++ /dev/null @@ -1,159 +0,0 @@ -"""Interface for passing domain configuration.""" - -import logging -from typing import ( - Optional, -) - -from ops.charm import ( - CharmBase, - RelationBrokenEvent, - RelationChangedEvent, - RelationEvent, -) -from ops.framework import ( - EventSource, - Object, - ObjectEvents, -) -from ops.model import ( - Relation, -) -import base64 -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "dfeee73ed0b248c29ed905aeda6fd417" - -# 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 - -class DomainConfigRequestEvent(RelationEvent): - """DomainConfigRequest Event.""" - pass - -class DomainConfigProviderEvents(ObjectEvents): - """Events class for `on`.""" - - remote_ready = EventSource(DomainConfigRequestEvent) - -class DomainConfigProvides(Object): - """DomainConfigProvides class.""" - - on = DomainConfigProviderEvents() - - def __init__(self, charm: CharmBase, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_domain_config_relation_changed, - ) - - def _on_domain_config_relation_changed( - self, event: RelationChangedEvent - ): - """Handle DomainConfig relation changed.""" - logging.debug("DomainConfig relation changed") - self.on.remote_ready.emit(event.relation) - - def set_domain_info( - self, domain_name: str, config_contents: str, ca=None - ) -> None: - """Set ceilometer configuration on the relation.""" - if not self.charm.unit.is_leader(): - logging.debug("Not a leader unit, skipping set config") - return - for relation in self.relations: - relation.data[self.charm.app]["domain-name"] = domain_name - relation.data[self.charm.app]["config-contents"] = base64.b64encode(config_contents.encode()).decode() - if ca: - relation.data[self.charm.app]["ca"] = base64.b64encode(ca.encode()).decode() - - @property - def relations(self): - return self.framework.model.relations[self.relation_name] - -class DomainConfigChangedEvent(RelationEvent): - """DomainConfigChanged Event.""" - - pass - - -class DomainConfigGoneAwayEvent(RelationBrokenEvent): - """DomainConfigGoneAway Event.""" - - pass - - -class DomainConfigRequirerEvents(ObjectEvents): - """Events class for `on`.""" - - config_changed = EventSource(DomainConfigChangedEvent) - goneaway = EventSource(DomainConfigGoneAwayEvent) - - -class DomainConfigRequires(Object): - """DomainConfigRequires class.""" - - on = DomainConfigRequirerEvents() - - def __init__(self, charm: CharmBase, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_domain_config_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_domain_config_relation_broken, - ) - - def _on_domain_config_relation_changed( - self, event: RelationChangedEvent - ): - """Handle DomainConfig relation changed.""" - logging.debug("DomainConfig config data changed") - self.on.config_changed.emit(event.relation) - - def _on_domain_config_relation_broken( - self, event: RelationBrokenEvent - ): - """Handle DomainConfig relation changed.""" - logging.debug("DomainConfig on_broken") - self.on.goneaway.emit(event.relation) - - def get_domain_configs(self, exclude=None): - exclude = exclude or [] - configs = [] - for relation in self.relations: - if relation in exclude: - continue - try: - domain_name = relation.data[relation.app].get("domain-name") - except KeyError: - logging.debug("Key error accessing app data") - continue - raw_config_contents = relation.data[relation.app].get("config-contents") - if not all([domain_name, raw_config_contents]): - continue - raw_ca = relation.data[relation.app].get("ca") - config = { - "domain-name": domain_name, - "config-contents": base64.b64decode(raw_config_contents).decode()} - if raw_ca: - config["ca"] = base64.b64decode(raw_ca).decode() - configs.append(config) - return configs - - @property - def relations(self): - return self.framework.model.relations[self.relation_name] - diff --git a/charms/keystone-ldap-k8s/osci.yaml b/charms/keystone-ldap-k8s/osci.yaml deleted file mode 100644 index c5faf352..00000000 --- a/charms/keystone-ldap-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: keystone-ldap-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/keystone-ldap-k8s/pyproject.toml b/charms/keystone-ldap-k8s/pyproject.toml deleted file mode 100644 index 2edc519a..00000000 --- a/charms/keystone-ldap-k8s/pyproject.toml +++ /dev/null @@ -1,33 +0,0 @@ -# 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] -line_length = 99 -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/charms/keystone-ldap-k8s/rename.sh b/charms/keystone-ldap-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/keystone-ldap-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/keystone-ldap-k8s/requirements.txt b/charms/keystone-ldap-k8s/requirements.txt index d4f4a3d0..f545ded9 100644 --- a/charms/keystone-ldap-k8s/requirements.txt +++ b/charms/keystone-ldap-k8s/requirements.txt @@ -12,6 +12,7 @@ lightkube-models ops pwgen -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam - python-keystoneclient # keystone-k8s + +# From ops_sunbeam +tenacity diff --git a/charms/keystone-ldap-k8s/src/charm.py b/charms/keystone-ldap-k8s/src/charm.py index fc4c9ede..26d3ae74 100755 --- a/charms/keystone-ldap-k8s/src/charm.py +++ b/charms/keystone-ldap-k8s/src/charm.py @@ -23,7 +23,11 @@ Send domain configuration to the keystone charm. """ import json import logging -from typing import Callable, List, Mapping +from typing import ( + Callable, + List, + Mapping, +) import charms.keystone_k8s.v0.domain_config as sunbeam_dc_svc import jinja2 @@ -31,7 +35,9 @@ import ops.charm import ops_sunbeam.charm as sunbeam_charm import ops_sunbeam.config_contexts as config_contexts import ops_sunbeam.relation_handlers as sunbeam_rhandlers -from ops.main import main +from ops.main import ( + main, +) # Log messages can be retrieved using juju debug-log logger = logging.getLogger(__name__) @@ -94,7 +100,9 @@ class KeystoneLDAPK8SCharm(sunbeam_charm.OSBaseOperatorCharm): def __init__(self, *args): super().__init__(*args) - def get_relation_handlers(self, handlers=None) -> List[sunbeam_rhandlers.RelationHandler]: + def get_relation_handlers( + self, handlers=None + ) -> List[sunbeam_rhandlers.RelationHandler]: """Relation handlers for the service.""" handlers = handlers or [] if self.can_add_handler(self.DOMAIN_CONFIG_RELATION_NAME, handlers): diff --git a/charms/keystone-ldap-k8s/test-requirements.txt b/charms/keystone-ldap-k8s/test-requirements.txt deleted file mode 100644 index 23e005bd..00000000 --- a/charms/keystone-ldap-k8s/test-requirements.txt +++ /dev/null @@ -1,17 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -pwgen -coverage -mock -flake8 -stestr -git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza -git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack -git+https://opendev.org/openstack/tempest.git#egg=tempest -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/keystone-ldap-k8s/tests/integration/test_charm.py b/charms/keystone-ldap-k8s/tests/integration/test_charm.py index 18f24d39..3911a065 100644 --- a/charms/keystone-ldap-k8s/tests/integration/test_charm.py +++ b/charms/keystone-ldap-k8s/tests/integration/test_charm.py @@ -1,14 +1,32 @@ #!/usr/bin/env python3 -# Copyright 2023 liam -# See LICENSE file for licensing details. + +# 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. + +"""Define keystone-ldap integration tests.""" import asyncio import logging -from pathlib import Path +from pathlib import ( + Path, +) import pytest import yaml -from pytest_operator.plugin import OpsTest +from pytest_operator.plugin import ( + OpsTest, +) logger = logging.getLogger(__name__) @@ -24,12 +42,21 @@ async def test_build_and_deploy(ops_test: OpsTest): """ # Build and deploy charm from local source folder charm = await ops_test.build_charm(".") - resources = {"httpbin-image": METADATA["resources"]["httpbin-image"]["upstream-source"]} + resources = { + "httpbin-image": METADATA["resources"]["httpbin-image"][ + "upstream-source" + ] + } # Deploy the charm and wait for active/idle status await asyncio.gather( - ops_test.model.deploy(await charm, resources=resources, application_name=APP_NAME), + ops_test.model.deploy( + await charm, resources=resources, application_name=APP_NAME + ), ops_test.model.wait_for_idle( - apps=[APP_NAME], status="active", raise_on_blocked=True, timeout=1000 + apps=[APP_NAME], + status="active", + raise_on_blocked=True, + timeout=1000, ), ) diff --git a/charms/keystone-ldap-k8s/tests/unit/test_keystone_ldap_charm.py b/charms/keystone-ldap-k8s/tests/unit/test_keystone_ldap_charm.py index 07c6a02d..3d0cf6cb 100644 --- a/charms/keystone-ldap-k8s/tests/unit/test_keystone_ldap_charm.py +++ b/charms/keystone-ldap-k8s/tests/unit/test_keystone_ldap_charm.py @@ -19,10 +19,11 @@ import base64 import json -import ops_sunbeam.test_utils as test_utils -from ops.testing import Harness - import charm +import ops_sunbeam.test_utils as test_utils +from ops.testing import ( + Harness, +) class _KeystoneLDAPK8SCharm(charm.KeystoneLDAPK8SCharm): @@ -45,6 +46,8 @@ class _KeystoneLDAPK8SCharm(charm.KeystoneLDAPK8SCharm): class TestKeystoneLDAPK8SCharm(test_utils.CharmTestCase): + """Create Keystone Ldap test charm.""" + def setUp(self): """Run test setup.""" self.harness = Harness(charm.KeystoneLDAPK8SCharm) @@ -56,7 +59,9 @@ class TestKeystoneLDAPK8SCharm(test_utils.CharmTestCase): self.harness.set_leader() rel_id = self.harness.add_relation("domain-config", "keystone") self.harness.add_relation_unit(rel_id, "keystone/0") - rel_data = self.harness.get_relation_data(rel_id, self.harness.charm.unit.app.name) + rel_data = self.harness.get_relation_data( + rel_id, self.harness.charm.unit.app.name + ) ldap_config_flags = json.dumps( { "group_tree_dn": "ou=groups,dc=test,dc=com", diff --git a/charms/keystone-ldap-k8s/tox.ini b/charms/keystone-ldap-k8s/tox.ini deleted file mode 100644 index 57750258..00000000 --- a/charms/keystone-ldap-k8s/tox.ini +++ /dev/null @@ -1,161 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - PYTHONPATH - HOME -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true;default-series= - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/magnum-k8s/.flake8 b/charms/magnum-k8s/.flake8 deleted file mode 100644 index 8ef84fcd..00000000 --- a/charms/magnum-k8s/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 99 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/charms/magnum-k8s/.gitignore b/charms/magnum-k8s/.gitignore deleted file mode 100644 index 33d25ac9..00000000 --- a/charms/magnum-k8s/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -venv/ -build/ -*.charm -.tox/ -.coverage -__pycache__/ -*.py[cod] -.idea -.vscode/ -.stestr/ diff --git a/charms/magnum-k8s/.gitreview b/charms/magnum-k8s/.gitreview deleted file mode 100644 index 6bdf9404..00000000 --- a/charms/magnum-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-magnum-k8s.git -defaultbranch=main diff --git a/charms/magnum-k8s/.jujuignore b/charms/magnum-k8s/.jujuignore deleted file mode 100644 index 6ccd559e..00000000 --- a/charms/magnum-k8s/.jujuignore +++ /dev/null @@ -1,3 +0,0 @@ -/venv -*.py[cod] -*.charm diff --git a/charms/magnum-k8s/.stestr.conf b/charms/magnum-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/magnum-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/magnum-k8s/.zuul.yaml b/charms/magnum-k8s/.zuul.yaml deleted file mode 100644 index 5509ca5f..00000000 --- a/charms/magnum-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: magnum-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/magnum-k8s/charmcraft.yaml b/charms/magnum-k8s/charmcraft.yaml index 3a9d1b7d..ea13253e 100644 --- a/charms/magnum-k8s/charmcraft.yaml +++ b/charms/magnum-k8s/charmcraft.yaml @@ -17,4 +17,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/magnum-k8s/fetch-libs.sh b/charms/magnum-k8s/fetch-libs.sh deleted file mode 100755 index a573dd64..00000000 --- a/charms/magnum-k8s/fetch-libs.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.data_platform_libs.v0.database_requires -charmcraft fetch-lib charms.keystone_k8s.v1.identity_service -charmcraft fetch-lib charms.keystone_k8s.v0.identity_resource -charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.traefik_k8s.v2.ingress diff --git a/charms/magnum-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/magnum-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 11ffd6ca..00000000 --- a/charms/magnum-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,537 +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. - -r"""[DEPRECATED] Relation 'requires' side abstraction for database relation. - -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.database_requires 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.database_requires 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, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 = 6 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(RelationEvent): - """Base class for database events.""" - - @property - def endpoints(self) -> Optional[str]: - """Returns a comma separated list of read/write endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("endpoints") - - @property - def password(self) -> Optional[str]: - """Returns the password for the created user.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("password") - - @property - def read_only_endpoints(self) -> Optional[str]: - """Returns a comma separated list of read only endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("read-only-endpoints") - - @property - def replset(self) -> Optional[str]: - """Returns the replicaset name. - - MongoDB only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("replset") - - @property - def tls(self) -> Optional[str]: - """Returns whether TLS is configured.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls") - - @property - def tls_ca(self) -> Optional[str]: - """Returns TLS CA.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls-ca") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("username") - - @property - def version(self) -> Optional[str]: - """Returns the version of the database. - - Version as informed by the database daemon. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("version") - - -class DatabaseCreatedEvent(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(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) - - -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 — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() # pyright: ignore [reportGeneralTypeIssues] - - def __init__( - self, - charm, - relation_name: str, - database_name: str, - extra_user_roles: Optional[str] = None, - relations_aliases: Optional[List[str]] = None, - ): - """Manager of database client relations.""" - super().__init__(charm, relation_name) - self.charm = charm - self.database = database_name - 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.relations_aliases = relations_aliases - 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 - ) - - # 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 _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. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].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"} - if event.app - else {} - ) - - # 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] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - 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 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"} - if relation.app - else {} - ) - 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 _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()) - getattr(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()) - getattr(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()) - getattr(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") - - @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]) diff --git a/charms/magnum-k8s/lib/charms/keystone_k8s/v0/identity_resource.py b/charms/magnum-k8s/lib/charms/keystone_k8s/v0/identity_resource.py deleted file mode 100644 index 6ef944ef..00000000 --- a/charms/magnum-k8s/lib/charms/keystone_k8s/v0/identity_resource.py +++ /dev/null @@ -1,373 +0,0 @@ -"""IdentityResourceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_ops interface. - -Import `IdentityResourceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_ops" - -Also provide additional parameters to the charm object: - - request - -Three events are also available to respond to: - - provider_ready - - provider_goneaway - - response_avaialable - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v0.identity_resource import IdentityResourceRequires - -class IdentityResourceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityResource Requires - self.identity_resource = IdentityResourceRequires( - self, "identity_ops", - ) - self.framework.observe( - self.identity_resource.on.provider_ready, self._on_identity_resource_ready) - self.framework.observe( - self.identity_resource.on.provider_goneaway, self._on_identity_resource_goneaway) - self.framework.observe( - self.identity_resource.on.response_available, self._on_identity_resource_response) - - def _on_identity_resource_ready(self, event): - '''React to the IdentityResource provider_ready event. - - This event happens when n IdentityResource relation is added to the - model. Ready to send any ops to keystone. - ''' - # Ready to send any ops. - pass - - def _on_identity_resource_response(self, event): - '''React to the IdentityResource response_available event. - - The IdentityResource interface will provide the response for the ops sent. - ''' - # Read the response for the ops sent. - pass - - def _on_identity_resource_goneaway(self, event): - '''React to the IdentityResource goneaway event. - - This event happens when an IdentityResource relation is removed. - ''' - # IdentityResource Relation has goneaway. No ops can be sent. - pass -``` - -A sample ops request can be of format -{ - "id": - "tag": - "ops": [ - { - "name": , - "params": { - : , - : - } - } - ] -} - -For any sensitive data in the ops params, the charm can create secrets and pass -secret id instead of sensitive data as part of ops request. The charm should -ensure to grant secret access to provider charm i.e., keystone over relation. -The secret content should hold the sensitive data with same name as param name. -""" - -import json -import logging - -from ops.charm import ( - RelationEvent, -) -from ops.framework import ( - EventBase, - EventSource, - Object, - ObjectEvents, - StoredState, -) -from ops.model import ( - Relation, -) - -logger = logging.getLogger(__name__) - - -# The unique Charmhub library identifier, never change it -LIBID = "b419d4d8249e423487daafc3665ed06f" - -# 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 = 2 - - -REQUEST_NOT_SENT = 1 -REQUEST_SENT = 2 -REQUEST_PROCESSED = 3 - - -class IdentityOpsProviderReadyEvent(RelationEvent): - """Has IdentityOpsProviderReady Event.""" - - pass - - -class IdentityOpsResponseEvent(RelationEvent): - """Has IdentityOpsResponse Event.""" - - pass - - -class IdentityOpsProviderGoneAwayEvent(RelationEvent): - """Has IdentityOpsProviderGoneAway Event.""" - - pass - - -class IdentityResourceResponseEvents(ObjectEvents): - """Events class for `on`.""" - - provider_ready = EventSource(IdentityOpsProviderReadyEvent) - response_available = EventSource(IdentityOpsResponseEvent) - provider_goneaway = EventSource(IdentityOpsProviderGoneAwayEvent) - - -class IdentityResourceRequires(Object): - """IdentityResourceRequires class.""" - - on = IdentityResourceResponseEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self._stored.set_default(provider_ready=False, requests=[]) - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_resource_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_resource_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_resource_relation_broken, - ) - - def _on_identity_resource_relation_joined(self, event): - """Handle IdentityResource joined.""" - self._stored.provider_ready = True - self.on.provider_ready.emit(event.relation) - - def _on_identity_resource_relation_changed(self, event): - """Handle IdentityResource changed.""" - id_ = self.response.get("id") - self.save_request_in_store(id_, None, None, REQUEST_PROCESSED) - self.on.response_available.emit(event.relation) - - def _on_identity_resource_relation_broken(self, event): - """Handle IdentityResource broken.""" - self._stored.provider_ready = False - self.on.provider_goneaway.emit(event.relation) - - @property - def _identity_resource_rel(self) -> Relation: - """The IdentityResource relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def response(self) -> dict: - """Response object from keystone.""" - response = self.get_remote_app_data("response") - if not response: - return {} - - try: - return json.loads(response) - except Exception as e: - logger.debug(str(e)) - - return {} - - def save_request_in_store(self, id: str, tag: str, ops: list, state: int): - """Save request in the store.""" - if id is None: - return - - for request in self._stored.requests: - if request.get("id") == id: - if tag: - request["tag"] = tag - if ops: - request["ops"] = ops - request["state"] = state - return - - # New request - self._stored.requests.append( - {"id": id, "tag": tag, "ops": ops, "state": state} - ) - - def get_request_from_store(self, id: str) -> dict: - """Get request from the stote.""" - for request in self._stored.requests: - if request.get("id") == id: - return request - - return {} - - def is_request_processed(self, id: str) -> bool: - """Check if request is processed.""" - for request in self._stored.requests: - if ( - request.get("id") == id - and request.get("state") == REQUEST_PROCESSED - ): - return True - - return False - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._identity_resource_rel.data[ - self._identity_resource_rel.app - ] - return data.get(key) - - def ready(self) -> bool: - """Interface is ready or not. - - Interface is considered ready if the op request is processed - and response is sent. In case of non leader unit, just consider - the interface is ready. - """ - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, set the interface to ready") - return True - - try: - app_data = self._identity_resource_rel.data[self.charm.app] - if "request" not in app_data: - return False - - request = json.loads(app_data["request"]) - request_id = request.get("id") - response_id = self.response.get("id") - if request_id == response_id: - return True - except Exception as e: - logger.debug(str(e)) - - return False - - def request_ops(self, request: dict) -> None: - """Request keystone ops.""" - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, not sending request") - return - - id_ = request.get("id") - tag = request.get("tag") - ops = request.get("ops") - req = self.get_request_from_store(id_) - if req and req.get("state") == REQUEST_PROCESSED: - logger.debug("Request {id_} already processed") - return - - if not self._stored.provider_ready: - self.save_request_in_store(id_, tag, ops, REQUEST_NOT_SENT) - logger.debug("Keystone not yet ready to take requests") - return - - logger.debug("Requesting ops to keystone") - app_data = self._identity_resource_rel.data[self.charm.app] - app_data["request"] = json.dumps(request) - self.save_request_in_store(id_, tag, ops, REQUEST_SENT) - - -class IdentityOpsRequestEvent(EventBase): - """Has IdentityOpsRequest Event.""" - - def __init__(self, handle, relation_id, relation_name, request): - """Initialise event.""" - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.request = request - - def snapshot(self): - """Snapshot the event.""" - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "request": self.request, - } - - def restore(self, snapshot): - """Restore the event.""" - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.request = snapshot["request"] - - -class IdentityResourceProviderEvents(ObjectEvents): - """Events class for `on`.""" - - process_op = EventSource(IdentityOpsRequestEvent) - - -class IdentityResourceProvides(Object): - """IdentityResourceProvides class.""" - - on = IdentityResourceProviderEvents() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_resource_relation_changed, - ) - - def _on_identity_resource_relation_changed(self, event): - """Handle IdentityResource changed.""" - request = event.relation.data[event.relation.app].get("request", {}) - self.on.process_op.emit( - event.relation.id, event.relation.name, request - ) - - def set_ops_response( - self, relation_id: str, relation_name: str, ops_response: dict - ): - """Set response to ops request.""" - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, not sending response") - return - - logger.debug("Update response from keystone") - _identity_resource_rel = self.charm.model.get_relation( - relation_name, relation_id - ) - if not _identity_resource_rel: - # Relation has disappeared so skip send of data - return - - app_data = _identity_resource_rel.data[self.charm.app] - app_data["response"] = json.dumps(ops_response) diff --git a/charms/magnum-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/magnum-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3f..00000000 --- a/charms/magnum-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -Also provide additional parameters to the charm object: - - service - - internal_url - - public_url - - admin_url - - region - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - service = "my-service" - internal_url = "http://internal-url" - public_url = "http://public-url" - admin_url = "http://admin-url" - region = "region" - ) - self.framework.observe( - self.identity_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# 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 - - -logger = logging.getLogger(__name__) - - -class IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._identity_service_rel.data[self._identity_service_rel.app] - return data.get(key) - - @property - def api_version(self) -> str: - """Return the api_version.""" - return self.get_remote_app_data('api-version') - - @property - def auth_host(self) -> str: - """Return the auth_host.""" - return self.get_remote_app_data('auth-host') - - @property - def auth_port(self) -> str: - """Return the auth_port.""" - return self.get_remote_app_data('auth-port') - - @property - def auth_protocol(self) -> str: - """Return the auth_protocol.""" - return self.get_remote_app_data('auth-protocol') - - @property - def internal_host(self) -> str: - """Return the internal_host.""" - return self.get_remote_app_data('internal-host') - - @property - def internal_port(self) -> str: - """Return the internal_port.""" - return self.get_remote_app_data('internal-port') - - @property - def internal_protocol(self) -> str: - """Return the internal_protocol.""" - return self.get_remote_app_data('internal-protocol') - - @property - def admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("password") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("username") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_credentials(self, relation_name: int, - relation_id: str, - api_version: str, - auth_host: str, - auth_port: str, - auth_protocol: str, - internal_host: str, - internal_port: str, - internal_protocol: str, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_rel.data[self.charm.app] - app_data["api-version"] = api_version - app_data["auth-host"] = auth_host - app_data["auth-port"] = str(auth_port) - app_data["auth-protocol"] = auth_protocol - app_data["internal-host"] = internal_host - app_data["internal-port"] = str(internal_port) - app_data["internal-protocol"] = internal_protocol - app_data["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/charms/magnum-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/magnum-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/magnum-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/magnum-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/magnum-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/magnum-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/magnum-k8s/osci.yaml b/charms/magnum-k8s/osci.yaml deleted file mode 100644 index c5db11f6..00000000 --- a/charms/magnum-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: magnum-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/magnum-k8s/pyproject.toml b/charms/magnum-k8s/pyproject.toml deleted file mode 100644 index e6de53b4..00000000 --- a/charms/magnum-k8s/pyproject.toml +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" - diff --git a/charms/magnum-k8s/rename.sh b/charms/magnum-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/magnum-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/magnum-k8s/requirements.txt b/charms/magnum-k8s/requirements.txt index 1dfcf975..20492790 100644 --- a/charms/magnum-k8s/requirements.txt +++ b/charms/magnum-k8s/requirements.txt @@ -1,6 +1,8 @@ ops jinja2 pydantic<2.0 -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam lightkube pwgen + +# From ops_sunbeam +tenacity diff --git a/charms/magnum-k8s/src/templates/magnum.conf.j2 b/charms/magnum-k8s/src/templates/magnum.conf.j2 index 6ef7b2ba..aa67c6da 100644 --- a/charms/magnum-k8s/src/templates/magnum.conf.j2 +++ b/charms/magnum-k8s/src/templates/magnum.conf.j2 @@ -5,11 +5,16 @@ state_path = /var/lib/magnum transport_url = {{ amqp.transport_url }} -{% include "parts/database-connection" %} +{% if database.connection -%} +sql_connection = {{ database.connection }} +{% endif -%} db_auto_create = false {% include "parts/section-identity" %} +[keystone_auth] +{% include "parts/identity-data" %} + {% include "parts/section-service-user" %} {% include "parts/section-trust" %} diff --git a/charms/magnum-k8s/src/templates/parts/database-connection b/charms/magnum-k8s/src/templates/parts/database-connection deleted file mode 100644 index 058d99ad..00000000 --- a/charms/magnum-k8s/src/templates/parts/database-connection +++ /dev/null @@ -1,3 +0,0 @@ -{% if database.connection -%} -sql_connection = {{ database.connection }} -{% endif -%} diff --git a/charms/magnum-k8s/src/templates/parts/identity-data b/charms/magnum-k8s/src/templates/parts/identity-data deleted file mode 100644 index db28064a..00000000 --- a/charms/magnum-k8s/src/templates/parts/identity-data +++ /dev/null @@ -1,26 +0,0 @@ -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True - -# XXX Region should come from the id relation here -region_name = {{ options.region }} diff --git a/charms/magnum-k8s/src/templates/parts/section-database b/charms/magnum-k8s/src/templates/parts/section-database deleted file mode 100644 index 986d9b10..00000000 --- a/charms/magnum-k8s/src/templates/parts/section-database +++ /dev/null @@ -1,3 +0,0 @@ -[database] -{% include "parts/database-connection" %} -connection_recycle_time = 200 diff --git a/charms/magnum-k8s/src/templates/parts/section-identity b/charms/magnum-k8s/src/templates/parts/section-identity deleted file mode 100644 index b45ab19a..00000000 --- a/charms/magnum-k8s/src/templates/parts/section-identity +++ /dev/null @@ -1,5 +0,0 @@ -[keystone_authtoken] -{% include "parts/identity-data" %} - -[keystone_auth] -{% include "parts/identity-data" %} diff --git a/charms/magnum-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/magnum-k8s/src/templates/parts/section-oslo-messaging-rabbit deleted file mode 100644 index 145c4ee9..00000000 --- a/charms/magnum-k8s/src/templates/parts/section-oslo-messaging-rabbit +++ /dev/null @@ -1,2 +0,0 @@ -[oslo_messaging_rabbit] -rabbit_quorum_queue = True diff --git a/charms/magnum-k8s/src/templates/parts/section-service-user b/charms/magnum-k8s/src/templates/parts/section-service-user deleted file mode 100644 index 65103693..00000000 --- a/charms/magnum-k8s/src/templates/parts/section-service-user +++ /dev/null @@ -1,17 +0,0 @@ -{% if identity_service.service_domain_id -%} -[service_user] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -send_service_user_token = true -auth_type = password -project_domain_id = {{ identity_service.service_domain_id }} -user_domain_id = {{ identity_service.service_domain_id }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -{% endif -%} diff --git a/charms/magnum-k8s/test-requirements.txt b/charms/magnum-k8s/test-requirements.txt deleted file mode 100644 index 23e005bd..00000000 --- a/charms/magnum-k8s/test-requirements.txt +++ /dev/null @@ -1,17 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -pwgen -coverage -mock -flake8 -stestr -git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza -git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack -git+https://opendev.org/openstack/tempest.git#egg=tempest -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/magnum-k8s/tests/unit/test_magnum_charm.py b/charms/magnum-k8s/tests/unit/test_magnum_charm.py index 6466f9a4..c157e546 100644 --- a/charms/magnum-k8s/tests/unit/test_magnum_charm.py +++ b/charms/magnum-k8s/tests/unit/test_magnum_charm.py @@ -18,6 +18,7 @@ import json +import charm import ops_sunbeam.test_utils as test_utils from mock import ( Mock, @@ -26,8 +27,6 @@ from ops.testing import ( Harness, ) -import charm - class _MagnumTestOperatorCharm(charm.MagnumOperatorCharm): """Test Operator Charm for Magnum Operator.""" diff --git a/charms/magnum-k8s/tox.ini b/charms/magnum-k8s/tox.ini deleted file mode 100644 index 57750258..00000000 --- a/charms/magnum-k8s/tox.ini +++ /dev/null @@ -1,161 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - PYTHONPATH - HOME -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true;default-series= - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/neutron-k8s/.flake8 b/charms/neutron-k8s/.flake8 deleted file mode 100644 index 8ef84fcd..00000000 --- a/charms/neutron-k8s/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 99 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/charms/neutron-k8s/.gitignore b/charms/neutron-k8s/.gitignore deleted file mode 100644 index 73f116c9..00000000 --- a/charms/neutron-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -*.charm -*.swp - -.coverage -__pycache__/ -*.py[cod] -.tox -.stestr/ -tempest.log diff --git a/charms/neutron-k8s/.gitreview b/charms/neutron-k8s/.gitreview deleted file mode 100644 index bd60210f..00000000 --- a/charms/neutron-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-neutron-k8s.git -defaultbranch=main diff --git a/charms/neutron-k8s/.jujuignore b/charms/neutron-k8s/.jujuignore deleted file mode 100644 index 6ccd559e..00000000 --- a/charms/neutron-k8s/.jujuignore +++ /dev/null @@ -1,3 +0,0 @@ -/venv -*.py[cod] -*.charm diff --git a/charms/neutron-k8s/.stestr.conf b/charms/neutron-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/neutron-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/neutron-k8s/.zuul.yaml b/charms/neutron-k8s/.zuul.yaml deleted file mode 100644 index 710da9f9..00000000 --- a/charms/neutron-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: neutron-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/neutron-k8s/charmcraft.yaml b/charms/neutron-k8s/charmcraft.yaml index 5ac17253..fa60d165 100644 --- a/charms/neutron-k8s/charmcraft.yaml +++ b/charms/neutron-k8s/charmcraft.yaml @@ -29,4 +29,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/neutron-k8s/fetch-libs.sh b/charms/neutron-k8s/fetch-libs.sh deleted file mode 100755 index 4f3bd45d..00000000 --- a/charms/neutron-k8s/fetch-libs.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.data_platform_libs.v0.database_requires -charmcraft fetch-lib charms.keystone_k8s.v1.identity_service -charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates -charmcraft fetch-lib charms.traefik_k8s.v2.ingress -charmcraft fetch-lib charms.ovn_central_k8s.v0.ovsdb diff --git a/charms/neutron-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/neutron-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 53d61912..00000000 --- a/charms/neutron-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,496 +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. - -"""Relation 'requires' side abstraction for database relation. - -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.database_requires import 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.database_requires import 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, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(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 password(self) -> Optional[str]: - """Returns the password for the created user.""" - return self.relation.data[self.relation.app].get("password") - - @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 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") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - return self.relation.data[self.relation.app].get("username") - - @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(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(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) - - -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 — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() - - 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) - self.charm = charm - self.database = database_name - 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.relations_aliases = relations_aliases - 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 - ) - - # 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 _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. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].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] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - 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 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) - - 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") - - @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]) diff --git a/charms/neutron-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/neutron-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3f..00000000 --- a/charms/neutron-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -Also provide additional parameters to the charm object: - - service - - internal_url - - public_url - - admin_url - - region - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - service = "my-service" - internal_url = "http://internal-url" - public_url = "http://public-url" - admin_url = "http://admin-url" - region = "region" - ) - self.framework.observe( - self.identity_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# 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 - - -logger = logging.getLogger(__name__) - - -class IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._identity_service_rel.data[self._identity_service_rel.app] - return data.get(key) - - @property - def api_version(self) -> str: - """Return the api_version.""" - return self.get_remote_app_data('api-version') - - @property - def auth_host(self) -> str: - """Return the auth_host.""" - return self.get_remote_app_data('auth-host') - - @property - def auth_port(self) -> str: - """Return the auth_port.""" - return self.get_remote_app_data('auth-port') - - @property - def auth_protocol(self) -> str: - """Return the auth_protocol.""" - return self.get_remote_app_data('auth-protocol') - - @property - def internal_host(self) -> str: - """Return the internal_host.""" - return self.get_remote_app_data('internal-host') - - @property - def internal_port(self) -> str: - """Return the internal_port.""" - return self.get_remote_app_data('internal-port') - - @property - def internal_protocol(self) -> str: - """Return the internal_protocol.""" - return self.get_remote_app_data('internal-protocol') - - @property - def admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("password") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("username") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_credentials(self, relation_name: int, - relation_id: str, - api_version: str, - auth_host: str, - auth_port: str, - auth_protocol: str, - internal_host: str, - internal_port: str, - internal_protocol: str, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_rel.data[self.charm.app] - app_data["api-version"] = api_version - app_data["auth-host"] = auth_host - app_data["auth-port"] = str(auth_port) - app_data["auth-protocol"] = auth_protocol - app_data["internal-host"] = internal_host - app_data["internal-port"] = str(internal_port) - app_data["internal-protocol"] = internal_protocol - app_data["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/charms/neutron-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/neutron-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/neutron-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/neutron-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/neutron-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/neutron-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/neutron-k8s/osci.yaml b/charms/neutron-k8s/osci.yaml deleted file mode 100644 index dba34983..00000000 --- a/charms/neutron-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: neutron-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/neutron-k8s/pyproject.toml b/charms/neutron-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/neutron-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/neutron-k8s/rename.sh b/charms/neutron-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/neutron-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/neutron-k8s/requirements.txt b/charms/neutron-k8s/requirements.txt index e988c61d..64399ba9 100644 --- a/charms/neutron-k8s/requirements.txt +++ b/charms/neutron-k8s/requirements.txt @@ -12,4 +12,5 @@ lightkube lightkube-models ops -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam +# From ops_sunbeam +tenacity diff --git a/charms/neutron-k8s/src/templates/parts/section-database b/charms/neutron-k8s/src/templates/parts/section-database deleted file mode 100644 index eb52f65e..00000000 --- a/charms/neutron-k8s/src/templates/parts/section-database +++ /dev/null @@ -1,7 +0,0 @@ -[database] -{% if database.connection -%} -connection = {{ database.connection }} -{% else -%} -connection = sqlite:////var/lib/cinder/cinder.db -{% endif -%} -connection_recycle_time = 200 diff --git a/charms/neutron-k8s/src/templates/parts/section-federation b/charms/neutron-k8s/src/templates/parts/section-federation deleted file mode 100644 index 65ee99ed..00000000 --- a/charms/neutron-k8s/src/templates/parts/section-federation +++ /dev/null @@ -1,10 +0,0 @@ -{% if trusted_dashboards %} -[federation] -{% for dashboard_url in trusted_dashboards -%} -trusted_dashboard = {{ dashboard_url }} -{% endfor -%} -{% endif %} -{% for sp in fid_sps -%} -[{{ sp['protocol-name'] }}] -remote_id_attribute = {{ sp['remote-id-attribute'] }} -{% endfor -%} diff --git a/charms/neutron-k8s/src/templates/parts/section-identity b/charms/neutron-k8s/src/templates/parts/section-identity deleted file mode 100644 index cbb1d069..00000000 --- a/charms/neutron-k8s/src/templates/parts/section-identity +++ /dev/null @@ -1,24 +0,0 @@ -[keystone_authtoken] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True diff --git a/charms/neutron-k8s/src/templates/parts/section-middleware b/charms/neutron-k8s/src/templates/parts/section-middleware deleted file mode 100644 index e65f1d98..00000000 --- a/charms/neutron-k8s/src/templates/parts/section-middleware +++ /dev/null @@ -1,6 +0,0 @@ -{% for section in sections -%} -[{{section}}] -{% for key, value in sections[section].items() -%} -{{ key }} = {{ value }} -{% endfor %} -{%- endfor %} diff --git a/charms/neutron-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/neutron-k8s/src/templates/parts/section-oslo-messaging-rabbit deleted file mode 100644 index 145c4ee9..00000000 --- a/charms/neutron-k8s/src/templates/parts/section-oslo-messaging-rabbit +++ /dev/null @@ -1,2 +0,0 @@ -[oslo_messaging_rabbit] -rabbit_quorum_queue = True diff --git a/charms/neutron-k8s/src/templates/parts/section-service-user b/charms/neutron-k8s/src/templates/parts/section-service-user deleted file mode 100644 index 65103693..00000000 --- a/charms/neutron-k8s/src/templates/parts/section-service-user +++ /dev/null @@ -1,17 +0,0 @@ -{% if identity_service.service_domain_id -%} -[service_user] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -send_service_user_token = true -auth_type = password -project_domain_id = {{ identity_service.service_domain_id }} -user_domain_id = {{ identity_service.service_domain_id }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -{% endif -%} diff --git a/charms/neutron-k8s/src/templates/parts/section-signing b/charms/neutron-k8s/src/templates/parts/section-signing deleted file mode 100644 index cb7d69ae..00000000 --- a/charms/neutron-k8s/src/templates/parts/section-signing +++ /dev/null @@ -1,15 +0,0 @@ -{% if enable_signing -%} -[signing] -{% if certfile -%} -certfile = {{ certfile }} -{% endif -%} -{% if keyfile -%} -keyfile = {{ keyfile }} -{% endif -%} -{% if ca_certs -%} -ca_certs = {{ ca_certs }} -{% endif -%} -{% if ca_key -%} -ca_key = {{ ca_key }} -{% endif -%} -{% endif -%} \ No newline at end of file diff --git a/charms/neutron-k8s/test-requirements.txt b/charms/neutron-k8s/test-requirements.txt deleted file mode 100644 index c533b127..00000000 --- a/charms/neutron-k8s/test-requirements.txt +++ /dev/null @@ -1,13 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -coverage -mock -flake8 -stestr -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/neutron-k8s/tests/unit/test_neutron_charm.py b/charms/neutron-k8s/tests/unit/test_neutron_charm.py index 606d0f09..ee850769 100644 --- a/charms/neutron-k8s/tests/unit/test_neutron_charm.py +++ b/charms/neutron-k8s/tests/unit/test_neutron_charm.py @@ -16,9 +16,8 @@ """Tests for neutron charm.""" -import ops_sunbeam.test_utils as test_utils - import charm +import ops_sunbeam.test_utils as test_utils class _NeutronOVNOperatorCharm(charm.NeutronOVNOperatorCharm): diff --git a/charms/neutron-k8s/tox.ini b/charms/neutron-k8s/tox.ini deleted file mode 100644 index fbaa02c5..00000000 --- a/charms/neutron-k8s/tox.ini +++ /dev/null @@ -1,169 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - HOME - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/nova-k8s/.flake8 b/charms/nova-k8s/.flake8 deleted file mode 100644 index 8ef84fcd..00000000 --- a/charms/nova-k8s/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 99 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/charms/nova-k8s/.gitignore b/charms/nova-k8s/.gitignore deleted file mode 100644 index 73f116c9..00000000 --- a/charms/nova-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -*.charm -*.swp - -.coverage -__pycache__/ -*.py[cod] -.tox -.stestr/ -tempest.log diff --git a/charms/nova-k8s/.gitreview b/charms/nova-k8s/.gitreview deleted file mode 100644 index 1f46f0d9..00000000 --- a/charms/nova-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-nova-k8s.git -defaultbranch=main diff --git a/charms/nova-k8s/.jujuignore b/charms/nova-k8s/.jujuignore deleted file mode 100644 index 6ccd559e..00000000 --- a/charms/nova-k8s/.jujuignore +++ /dev/null @@ -1,3 +0,0 @@ -/venv -*.py[cod] -*.charm diff --git a/charms/nova-k8s/.stestr.conf b/charms/nova-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/nova-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/nova-k8s/.zuul.yaml b/charms/nova-k8s/.zuul.yaml deleted file mode 100644 index 619607cb..00000000 --- a/charms/nova-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: nova-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/nova-k8s/charmcraft.yaml b/charms/nova-k8s/charmcraft.yaml index 0c6b3bdb..cbd210b0 100644 --- a/charms/nova-k8s/charmcraft.yaml +++ b/charms/nova-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/nova-k8s/fetch-libs.sh b/charms/nova-k8s/fetch-libs.sh deleted file mode 100755 index 896bde54..00000000 --- a/charms/nova-k8s/fetch-libs.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.data_platform_libs.v0.database_requires -charmcraft fetch-lib charms.keystone_k8s.v1.identity_service -charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.traefik_k8s.v2.ingress -charmcraft fetch-lib charms.nova_compute_k8s.v0.cloud_compute diff --git a/charms/nova-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/nova-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 53d61912..00000000 --- a/charms/nova-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,496 +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. - -"""Relation 'requires' side abstraction for database relation. - -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.database_requires import 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.database_requires import 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, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(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 password(self) -> Optional[str]: - """Returns the password for the created user.""" - return self.relation.data[self.relation.app].get("password") - - @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 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") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - return self.relation.data[self.relation.app].get("username") - - @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(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(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) - - -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 — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() - - 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) - self.charm = charm - self.database = database_name - 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.relations_aliases = relations_aliases - 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 - ) - - # 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 _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. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].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] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - 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 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) - - 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") - - @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]) diff --git a/charms/nova-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/nova-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3f..00000000 --- a/charms/nova-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -Also provide additional parameters to the charm object: - - service - - internal_url - - public_url - - admin_url - - region - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - service = "my-service" - internal_url = "http://internal-url" - public_url = "http://public-url" - admin_url = "http://admin-url" - region = "region" - ) - self.framework.observe( - self.identity_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# 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 - - -logger = logging.getLogger(__name__) - - -class IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._identity_service_rel.data[self._identity_service_rel.app] - return data.get(key) - - @property - def api_version(self) -> str: - """Return the api_version.""" - return self.get_remote_app_data('api-version') - - @property - def auth_host(self) -> str: - """Return the auth_host.""" - return self.get_remote_app_data('auth-host') - - @property - def auth_port(self) -> str: - """Return the auth_port.""" - return self.get_remote_app_data('auth-port') - - @property - def auth_protocol(self) -> str: - """Return the auth_protocol.""" - return self.get_remote_app_data('auth-protocol') - - @property - def internal_host(self) -> str: - """Return the internal_host.""" - return self.get_remote_app_data('internal-host') - - @property - def internal_port(self) -> str: - """Return the internal_port.""" - return self.get_remote_app_data('internal-port') - - @property - def internal_protocol(self) -> str: - """Return the internal_protocol.""" - return self.get_remote_app_data('internal-protocol') - - @property - def admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("password") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("username") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_credentials(self, relation_name: int, - relation_id: str, - api_version: str, - auth_host: str, - auth_port: str, - auth_protocol: str, - internal_host: str, - internal_port: str, - internal_protocol: str, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_rel.data[self.charm.app] - app_data["api-version"] = api_version - app_data["auth-host"] = auth_host - app_data["auth-port"] = str(auth_port) - app_data["auth-protocol"] = auth_protocol - app_data["internal-host"] = internal_host - app_data["internal-port"] = str(internal_port) - app_data["internal-protocol"] = internal_protocol - app_data["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/charms/nova-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/nova-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/nova-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/nova-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/nova-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/nova-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/nova-k8s/osci.yaml b/charms/nova-k8s/osci.yaml deleted file mode 100644 index d707a693..00000000 --- a/charms/nova-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: nova-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/nova-k8s/pyproject.toml b/charms/nova-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/nova-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/nova-k8s/rename.sh b/charms/nova-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/nova-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/nova-k8s/requirements.txt b/charms/nova-k8s/requirements.txt index 408e0a2d..efcc785e 100644 --- a/charms/nova-k8s/requirements.txt +++ b/charms/nova-k8s/requirements.txt @@ -12,4 +12,6 @@ lightkube lightkube-models ops -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam + +# From ops_sunbeam +tenacity diff --git a/charms/nova-k8s/src/templates/nova.conf.j2 b/charms/nova-k8s/src/templates/nova.conf.j2 index 8f49f109..a42983fa 100644 --- a/charms/nova-k8s/src/templates/nova.conf.j2 +++ b/charms/nova-k8s/src/templates/nova.conf.j2 @@ -13,12 +13,7 @@ connection = sqlite:////var/lib/nova/nova_api.sqlite {% endif -%} connection_recycle_time = 200 -[database] -{% if database.connection -%} -connection = {{ database.connection }} -{% else -%} -connection = sqlite:////var/lib/nova/nova.sqlite -{% endif -%} +{% include "parts/section-database" %} [glance] service_type = image @@ -26,14 +21,16 @@ service_name = glance valid_interfaces = admin region_name = {{ options.region }} -[keystone_authtoken] {% include "parts/section-identity" %} +region_name = {{ options.region }} [neutron] -{% include "parts/section-identity" %} +{% include "parts/identity-data" %} +region_name = {{ options.region }} [placement] -{% include "parts/section-identity" %} +{% include "parts/identity-data" %} +region_name = {{ options.region }} {% include "parts/section-service-user" %} diff --git a/charms/nova-k8s/src/templates/parts/section-identity b/charms/nova-k8s/src/templates/parts/section-identity deleted file mode 100644 index 5a48d675..00000000 --- a/charms/nova-k8s/src/templates/parts/section-identity +++ /dev/null @@ -1,25 +0,0 @@ -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -# XXX Region should come from the id relation here -region_name = {{ options.region }} -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True diff --git a/charms/nova-k8s/src/templates/parts/section-oslo-messaging-rabbit b/charms/nova-k8s/src/templates/parts/section-oslo-messaging-rabbit deleted file mode 100644 index 145c4ee9..00000000 --- a/charms/nova-k8s/src/templates/parts/section-oslo-messaging-rabbit +++ /dev/null @@ -1,2 +0,0 @@ -[oslo_messaging_rabbit] -rabbit_quorum_queue = True diff --git a/charms/nova-k8s/src/templates/parts/section-service-user b/charms/nova-k8s/src/templates/parts/section-service-user deleted file mode 100644 index 65103693..00000000 --- a/charms/nova-k8s/src/templates/parts/section-service-user +++ /dev/null @@ -1,17 +0,0 @@ -{% if identity_service.service_domain_id -%} -[service_user] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -send_service_user_token = true -auth_type = password -project_domain_id = {{ identity_service.service_domain_id }} -user_domain_id = {{ identity_service.service_domain_id }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -{% endif -%} diff --git a/charms/nova-k8s/test-requirements.txt b/charms/nova-k8s/test-requirements.txt deleted file mode 100644 index a9b0d698..00000000 --- a/charms/nova-k8s/test-requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -coverage -mock -flake8 -stestr -git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza -git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack -git+https://opendev.org/openstack/tempest.git#egg=tempest -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/nova-k8s/tests/unit/test_nova_charm.py b/charms/nova-k8s/tests/unit/test_nova_charm.py index c9adf611..a9285295 100644 --- a/charms/nova-k8s/tests/unit/test_nova_charm.py +++ b/charms/nova-k8s/tests/unit/test_nova_charm.py @@ -16,9 +16,8 @@ """Unit tests for Nova operator.""" -import ops_sunbeam.test_utils as test_utils - import charm +import ops_sunbeam.test_utils as test_utils class _NovaTestOperatorCharm(charm.NovaOperatorCharm): diff --git a/charms/nova-k8s/tox.ini b/charms/nova-k8s/tox.ini deleted file mode 100644 index f6fb644e..00000000 --- a/charms/nova-k8s/tox.ini +++ /dev/null @@ -1,161 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - HOME - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true;update-status-hook-interval=1m - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/octavia-k8s/.gitignore b/charms/octavia-k8s/.gitignore deleted file mode 100644 index 24ff2e41..00000000 --- a/charms/octavia-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -*.charm -.tox/ -.coverage -__pycache__/ -*.py[cod] -.idea -.vscode/ -*.swp -.stestr/ diff --git a/charms/octavia-k8s/.gitreview b/charms/octavia-k8s/.gitreview deleted file mode 100644 index 26336e73..00000000 --- a/charms/octavia-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-octavia-k8s.git -defaultbranch=main diff --git a/charms/octavia-k8s/.stestr.conf b/charms/octavia-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/octavia-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/octavia-k8s/.zuul.yaml b/charms/octavia-k8s/.zuul.yaml deleted file mode 100644 index fc2ae5c1..00000000 --- a/charms/octavia-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: octavia-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/octavia-k8s/charmcraft.yaml b/charms/octavia-k8s/charmcraft.yaml index 2fdc318d..9556149e 100644 --- a/charms/octavia-k8s/charmcraft.yaml +++ b/charms/octavia-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/octavia-k8s/fetch-libs.sh b/charms/octavia-k8s/fetch-libs.sh deleted file mode 100755 index fbd3ab38..00000000 --- a/charms/octavia-k8s/fetch-libs.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.data_platform_libs.v0.database_requires -charmcraft fetch-lib charms.keystone_k8s.v0.identity_resource -charmcraft fetch-lib charms.keystone_k8s.v1.identity_service -charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates -charmcraft fetch-lib charms.traefik_k8s.v2.ingress -charmcraft fetch-lib charms.ovn_central_k8s.v0.ovsdb diff --git a/charms/octavia-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/octavia-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 11ffd6ca..00000000 --- a/charms/octavia-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,537 +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. - -r"""[DEPRECATED] Relation 'requires' side abstraction for database relation. - -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.database_requires 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.database_requires 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, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 = 6 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(RelationEvent): - """Base class for database events.""" - - @property - def endpoints(self) -> Optional[str]: - """Returns a comma separated list of read/write endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("endpoints") - - @property - def password(self) -> Optional[str]: - """Returns the password for the created user.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("password") - - @property - def read_only_endpoints(self) -> Optional[str]: - """Returns a comma separated list of read only endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("read-only-endpoints") - - @property - def replset(self) -> Optional[str]: - """Returns the replicaset name. - - MongoDB only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("replset") - - @property - def tls(self) -> Optional[str]: - """Returns whether TLS is configured.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls") - - @property - def tls_ca(self) -> Optional[str]: - """Returns TLS CA.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls-ca") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("username") - - @property - def version(self) -> Optional[str]: - """Returns the version of the database. - - Version as informed by the database daemon. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("version") - - -class DatabaseCreatedEvent(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(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) - - -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 — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() # pyright: ignore [reportGeneralTypeIssues] - - def __init__( - self, - charm, - relation_name: str, - database_name: str, - extra_user_roles: Optional[str] = None, - relations_aliases: Optional[List[str]] = None, - ): - """Manager of database client relations.""" - super().__init__(charm, relation_name) - self.charm = charm - self.database = database_name - 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.relations_aliases = relations_aliases - 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 - ) - - # 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 _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. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].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"} - if event.app - else {} - ) - - # 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] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - 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 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"} - if relation.app - else {} - ) - 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 _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()) - getattr(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()) - getattr(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()) - getattr(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") - - @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]) diff --git a/charms/octavia-k8s/lib/charms/keystone_k8s/v0/identity_resource.py b/charms/octavia-k8s/lib/charms/keystone_k8s/v0/identity_resource.py deleted file mode 100644 index 154fab83..00000000 --- a/charms/octavia-k8s/lib/charms/keystone_k8s/v0/identity_resource.py +++ /dev/null @@ -1,392 +0,0 @@ -"""IdentityResourceProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the identity_ops interface. - -Import `IdentityResourceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_ops" - -Also provide additional parameters to the charm object: - - request - -Three events are also available to respond to: - - provider_ready - - provider_goneaway - - response_avaialable - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v0.identity_resource import IdentityResourceRequires - -class IdentityResourceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityResource Requires - self.identity_resource = IdentityResourceRequires( - self, "identity_ops", - ) - self.framework.observe( - self.identity_resource.on.provider_ready, self._on_identity_resource_ready) - self.framework.observe( - self.identity_resource.on.provider_goneaway, self._on_identity_resource_goneaway) - self.framework.observe( - self.identity_resource.on.response_available, self._on_identity_resource_response) - - def _on_identity_resource_ready(self, event): - '''React to the IdentityResource provider_ready event. - - This event happens when n IdentityResource relation is added to the - model. Ready to send any ops to keystone. - ''' - # Ready to send any ops. - pass - - def _on_identity_resource_response(self, event): - '''React to the IdentityResource response_available event. - - The IdentityResource interface will provide the response for the ops sent. - ''' - # Read the response for the ops sent. - pass - - def _on_identity_resource_goneaway(self, event): - '''React to the IdentityResource goneaway event. - - This event happens when an IdentityResource relation is removed. - ''' - # IdentityResource Relation has goneaway. No ops can be sent. - pass -``` - -A sample ops request can be of format -{ - "id": - "tag": - "ops": [ - { - "name": , - "params": { - : , - : - } - } - ] -} - -For any sensitive data in the ops params, the charm can create secrets and pass -secret id instead of sensitive data as part of ops request. The charm should -ensure to grant secret access to provider charm i.e., keystone over relation. -The secret content should hold the sensitive data with same name as param name. -""" - -import json -import logging -from typing import ( - Optional, -) - -from ops.charm import ( - CharmBase, - RelationBrokenEvent, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import ( - EventBase, - EventSource, - Object, - ObjectEvents, - StoredState, -) -from ops.model import ( - Relation, -) - -logger = logging.getLogger(__name__) - - -# The unique Charmhub library identifier, never change it -LIBID = "b419d4d8249e423487daafc3665ed06f" - -# 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 - - -REQUEST_NOT_SENT = 1 -REQUEST_SENT = 2 -REQUEST_PROCESSED = 3 - - -class IdentityOpsProviderReadyEvent(RelationEvent): - """Has IdentityOpsProviderReady Event.""" - - pass - - -class IdentityOpsResponseEvent(RelationEvent): - """Has IdentityOpsResponse Event.""" - - pass - - -class IdentityOpsProviderGoneAwayEvent(RelationEvent): - """Has IdentityOpsProviderGoneAway Event.""" - - pass - - -class IdentityResourceResponseEvents(ObjectEvents): - """Events class for `on`.""" - - provider_ready = EventSource(IdentityOpsProviderReadyEvent) - response_available = EventSource(IdentityOpsResponseEvent) - provider_goneaway = EventSource(IdentityOpsProviderGoneAwayEvent) - - -class IdentityResourceRequires(Object): - """IdentityResourceRequires class.""" - - on = IdentityResourceResponseEvents() - _stored = StoredState() - - def __init__(self, charm: CharmBase, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self._stored.set_default(provider_ready=False, requests=[]) - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_resource_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_resource_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_resource_relation_broken, - ) - - def _on_identity_resource_relation_joined( - self, event: RelationJoinedEvent - ): - """Handle IdentityResource joined.""" - self._stored.provider_ready = True - self.on.provider_ready.emit(event.relation) - - def _on_identity_resource_relation_changed( - self, event: RelationChangedEvent - ): - """Handle IdentityResource changed.""" - id_ = self.response.get("id") - self.save_request_in_store(id_, None, None, REQUEST_PROCESSED) - self.on.response_available.emit(event.relation) - - def _on_identity_resource_relation_broken( - self, event: RelationBrokenEvent - ): - """Handle IdentityResource broken.""" - self._stored.provider_ready = False - self.on.provider_goneaway.emit(event.relation) - - @property - def _identity_resource_rel(self) -> Optional[Relation]: - """The IdentityResource relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def response(self) -> dict: - """Response object from keystone.""" - response = self.get_remote_app_data("response") - if not response: - return {} - - try: - return json.loads(response) - except Exception as e: - logger.debug(str(e)) - - return {} - - def save_request_in_store( - self, id: str, tag: str, ops: list, state: int - ) -> None: - """Save request in the store.""" - if id is None: - return - - for request in self._stored.requests: - if request.get("id") == id: - if tag: - request["tag"] = tag - if ops: - request["ops"] = ops - request["state"] = state - return - - # New request - self._stored.requests.append( - {"id": id, "tag": tag, "ops": ops, "state": state} - ) - - def get_request_from_store(self, id: str) -> dict: - """Get request from the stote.""" - for request in self._stored.requests: - if request.get("id") == id: - return request - - return {} - - def is_request_processed(self, id: str) -> bool: - """Check if request is processed.""" - for request in self._stored.requests: - if ( - request.get("id") == id - and request.get("state") == REQUEST_PROCESSED - ): - return True - - return False - - def get_remote_app_data(self, key: str) -> Optional[str]: - """Return the value for the given key from remote app data.""" - if self._identity_resource_rel: - data = self._identity_resource_rel.data[ - self._identity_resource_rel.app - ] - return data.get(key) - - return None - - def ready(self) -> bool: - """Interface is ready or not. - - Interface is considered ready if the op request is processed - and response is sent. In case of non leader unit, just consider - the interface is ready. - """ - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, set the interface to ready") - return True - - try: - app_data = self._identity_resource_rel.data[self.charm.app] - if "request" not in app_data: - return False - - request = json.loads(app_data["request"]) - request_id = request.get("id") - response_id = self.response.get("id") - if request_id == response_id: - return True - except Exception as e: - logger.debug(str(e)) - - return False - - def request_ops(self, request: dict) -> None: - """Request keystone ops.""" - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, not sending request") - return - - id_ = request.get("id") - tag = request.get("tag") - ops = request.get("ops") - req = self.get_request_from_store(id_) - if req and req.get("state") == REQUEST_PROCESSED: - logger.debug("Request {id_} already processed") - return - - if not self._stored.provider_ready: - self.save_request_in_store(id_, tag, ops, REQUEST_NOT_SENT) - logger.debug("Keystone not yet ready to take requests") - return - - logger.debug("Requesting ops to keystone") - app_data = self._identity_resource_rel.data[self.charm.app] - app_data["request"] = json.dumps(request) - self.save_request_in_store(id_, tag, ops, REQUEST_SENT) - - -class IdentityOpsRequestEvent(EventBase): - """Has IdentityOpsRequest Event.""" - - def __init__(self, handle, relation_id, relation_name, request): - """Initialise event.""" - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.request = request - - def snapshot(self): - """Snapshot the event.""" - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "request": self.request, - } - - def restore(self, snapshot): - """Restore the event.""" - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.request = snapshot["request"] - - -class IdentityResourceProviderEvents(ObjectEvents): - """Events class for `on`.""" - - process_op = EventSource(IdentityOpsRequestEvent) - - -class IdentityResourceProvides(Object): - """IdentityResourceProvides class.""" - - on = IdentityResourceProviderEvents() - - def __init__(self, charm: CharmBase, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_resource_relation_changed, - ) - - def _on_identity_resource_relation_changed( - self, event: RelationChangedEvent - ): - """Handle IdentityResource changed.""" - request = event.relation.data[event.relation.app].get("request", {}) - self.on.process_op.emit( - event.relation.id, event.relation.name, request - ) - - def set_ops_response( - self, relation_id: str, relation_name: str, ops_response: dict - ) -> None: - """Set response to ops request.""" - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, not sending response") - return - - logger.debug("Update response from keystone") - _identity_resource_rel = self.charm.model.get_relation( - relation_name, relation_id - ) - if not _identity_resource_rel: - # Relation has disappeared so skip send of data - return - - app_data = _identity_resource_rel.data[self.charm.app] - app_data["response"] = json.dumps(ops_response) diff --git a/charms/octavia-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/octavia-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3f..00000000 --- a/charms/octavia-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -Also provide additional parameters to the charm object: - - service - - internal_url - - public_url - - admin_url - - region - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - service = "my-service" - internal_url = "http://internal-url" - public_url = "http://public-url" - admin_url = "http://admin-url" - region = "region" - ) - self.framework.observe( - self.identity_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# 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 - - -logger = logging.getLogger(__name__) - - -class IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._identity_service_rel.data[self._identity_service_rel.app] - return data.get(key) - - @property - def api_version(self) -> str: - """Return the api_version.""" - return self.get_remote_app_data('api-version') - - @property - def auth_host(self) -> str: - """Return the auth_host.""" - return self.get_remote_app_data('auth-host') - - @property - def auth_port(self) -> str: - """Return the auth_port.""" - return self.get_remote_app_data('auth-port') - - @property - def auth_protocol(self) -> str: - """Return the auth_protocol.""" - return self.get_remote_app_data('auth-protocol') - - @property - def internal_host(self) -> str: - """Return the internal_host.""" - return self.get_remote_app_data('internal-host') - - @property - def internal_port(self) -> str: - """Return the internal_port.""" - return self.get_remote_app_data('internal-port') - - @property - def internal_protocol(self) -> str: - """Return the internal_protocol.""" - return self.get_remote_app_data('internal-protocol') - - @property - def admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("password") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("username") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_credentials(self, relation_name: int, - relation_id: str, - api_version: str, - auth_host: str, - auth_port: str, - auth_protocol: str, - internal_host: str, - internal_port: str, - internal_protocol: str, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_rel.data[self.charm.app] - app_data["api-version"] = api_version - app_data["auth-host"] = auth_host - app_data["auth-port"] = str(auth_port) - app_data["auth-protocol"] = auth_protocol - app_data["internal-host"] = internal_host - app_data["internal-port"] = str(internal_port) - app_data["internal-protocol"] = internal_protocol - app_data["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/charms/octavia-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py b/charms/octavia-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py deleted file mode 100644 index 732679a6..00000000 --- a/charms/octavia-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py +++ /dev/null @@ -1,206 +0,0 @@ -"""TODO: Add a proper docstring here. - -This is a placeholder docstring for this charm library. Docstrings are -presented on Charmhub and updated whenever you push a new version of the -library. - -Complete documentation about creating and documenting libraries can be found -in the SDK docs at https://juju.is/docs/sdk/libraries. - -See `charmcraft publish-lib` and `charmcraft fetch-lib` for details of how to -share and consume charm libraries. They serve to enhance collaboration -between charmers. Use a charmer's libraries for classes that handle -integration with their charm. - -Bear in mind that new revisions of the different major API versions (v0, v1, -v2 etc) are maintained independently. You can continue to update v0 and v1 -after you have pushed v3. - -Markdown is supported, following the CommonMark specification. -""" - -import logging -import typing -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -# The unique Charmhub library identifier, never change it -LIBID = "114b7bb1970445daa61650e451f9da62" - -# Increment this major API version when introducing breaking changes -LIBAPI = 0 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 3 - - -# TODO: add your code here! Happy coding! -class OVSDBCMSConnectedEvent(EventBase): - """OVSDBCMS connected Event.""" - - pass - - -class OVSDBCMSReadyEvent(EventBase): - """OVSDBCMS ready for use Event.""" - - pass - - -class OVSDBCMSGoneAwayEvent(EventBase): - """OVSDBCMS relation has gone-away Event""" - - pass - - -class OVSDBCMSServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(OVSDBCMSConnectedEvent) - ready = EventSource(OVSDBCMSReadyEvent) - goneaway = EventSource(OVSDBCMSGoneAwayEvent) - - -class OVSDBCMSRequires(Object): - """ - OVSDBCMSRequires class - """ - - on = OVSDBCMSServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_ovsdb_cms_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_ovsdb_cms_relation_broken, - ) - - def _on_ovsdb_cms_relation_joined(self, event): - """OVSDBCMS relation joined.""" - logging.debug("OVSDBCMSRequires on_joined") - self.on.connected.emit() - - def bound_hostnames(self): - return self.get_all_unit_values("bound-hostname") - - def bound_addresses(self): - return self.get_all_unit_values("bound-address") - - def remote_ready(self): - return all(self.bound_hostnames()) or all(self.bound_addresses()) - - def _on_ovsdb_cms_relation_changed(self, event): - """OVSDBCMS relation changed.""" - logging.debug("OVSDBCMSRequires on_changed") - if self.remote_ready(): - self.on.ready.emit() - - def _on_ovsdb_cms_relation_broken(self, event): - """OVSDBCMS relation broken.""" - logging.debug("OVSDBCMSRequires on_broken") - self.on.goneaway.emit() - - def get_all_unit_values(self, key: str) -> typing.List[str]: - """Retrieve value for key from all related units.""" - values = [] - relation = self.framework.model.get_relation(self.relation_name) - if relation: - for unit in relation.units: - values.append(relation.data[unit].get(key)) - return values - - - -class OVSDBCMSClientConnectedEvent(EventBase): - """OVSDBCMS connected Event.""" - - pass - - -class OVSDBCMSClientReadyEvent(EventBase): - """OVSDBCMS ready for use Event.""" - - pass - - -class OVSDBCMSClientGoneAwayEvent(EventBase): - """OVSDBCMS relation has gone-away Event""" - - pass - - -class OVSDBCMSClientEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(OVSDBCMSClientConnectedEvent) - ready = EventSource(OVSDBCMSClientReadyEvent) - goneaway = EventSource(OVSDBCMSClientGoneAwayEvent) - - -class OVSDBCMSProvides(Object): - """ - OVSDBCMSProvides class - """ - - on = OVSDBCMSClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_ovsdb_cms_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_ovsdb_cms_relation_broken, - ) - - def _on_ovsdb_cms_relation_joined(self, event): - """Handle ovsdb-cms joined.""" - logging.debug("OVSDBCMSProvides on_joined") - self.on.connected.emit() - - def _on_ovsdb_cms_relation_changed(self, event): - """Handle ovsdb-cms changed.""" - logging.debug("OVSDBCMSProvides on_changed") - self.on.ready.emit() - - def _on_ovsdb_cms_relation_broken(self, event): - """Handle ovsdb-cms broken.""" - logging.debug("OVSDBCMSProvides on_departed") - self.on.goneaway.emit() - - def set_unit_data(self, settings: typing.Dict[str, str]) -> None: - """Publish settings on the peer unit data bag.""" - relations = self.framework.model.relations[self.relation_name] - for relation in relations: - for k, v in settings.items(): - relation.data[self.model.unit][k] = v diff --git a/charms/octavia-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py b/charms/octavia-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py deleted file mode 100644 index be171d8e..00000000 --- a/charms/octavia-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py +++ /dev/null @@ -1,1360 +0,0 @@ -# Copyright 2021 Canonical Ltd. -# See LICENSE file for licensing details. - - -"""Library for the tls-certificates relation. - -This library contains the Requires and Provides classes for handling the tls-certificates -interface. - -## Getting Started -From a charm directory, fetch the library using `charmcraft`: - -```shell -charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates -``` - -Add the following libraries to the charm's `requirements.txt` file: -- jsonschema -- cryptography - -Add the following section to the charm's `charmcraft.yaml` file: -```yaml -parts: - charm: - build-packages: - - libffi-dev - - libssl-dev - - rustc - - cargo -``` - -### Provider charm -The provider charm is the charm providing certificates to another charm that requires them. In -this example, the provider charm is storing its private key using a peer relation interface called -`replicas`. - -Example: -```python -from charms.tls_certificates_interface.v1.tls_certificates import ( - CertificateCreationRequestEvent, - CertificateRevocationRequestEvent, - TLSCertificatesProvidesV1, - generate_private_key, -) -from ops.charm import CharmBase, InstallEvent -from ops.main import main -from ops.model import ActiveStatus, WaitingStatus - - -def generate_ca(private_key: bytes, subject: str) -> str: - return "whatever ca content" - - -def generate_certificate(ca: str, private_key: str, csr: str) -> str: - return "Whatever certificate" - - -class ExampleProviderCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - self.certificates = TLSCertificatesProvidesV1(self, "certificates") - self.framework.observe( - self.certificates.on.certificate_request, self._on_certificate_request - ) - self.framework.observe( - self.certificates.on.certificate_revoked, self._on_certificate_revocation_request - ) - self.framework.observe(self.on.install, self._on_install) - - def _on_install(self, event: InstallEvent) -> None: - private_key_password = b"banana" - private_key = generate_private_key(password=private_key_password) - ca_certificate = generate_ca(private_key=private_key, subject="whatever") - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update( - { - "private_key_password": "banana", - "private_key": private_key, - "ca_certificate": ca_certificate, - } - ) - self.unit.status = ActiveStatus() - - def _on_certificate_request(self, event: CertificateCreationRequestEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - ca_certificate = replicas_relation.data[self.app].get("ca_certificate") - private_key = replicas_relation.data[self.app].get("private_key") - certificate = generate_certificate( - ca=ca_certificate, - private_key=private_key, - csr=event.certificate_signing_request, - ) - - self.certificates.set_relation_certificate( - certificate=certificate, - certificate_signing_request=event.certificate_signing_request, - ca=ca_certificate, - chain=[ca_certificate, certificate], - relation_id=event.relation_id, - ) - - def _on_certificate_revocation_request(self, event: CertificateRevocationRequestEvent) -> None: - # Do what you want to do with this information - pass - - -if __name__ == "__main__": - main(ExampleProviderCharm) -``` - -### Requirer charm -The requirer charm is the charm requiring certificates from another charm that provides them. In -this example, the requirer charm is storing its certificates using a peer relation interface called -`replicas`. - -Example: -```python -from charms.tls_certificates_interface.v1.tls_certificates import ( - CertificateAvailableEvent, - CertificateExpiringEvent, - CertificateRevokedEvent, - TLSCertificatesRequiresV1, - generate_csr, - generate_private_key, -) -from ops.charm import CharmBase, RelationJoinedEvent -from ops.main import main -from ops.model import ActiveStatus, WaitingStatus - - -class ExampleRequirerCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - self.cert_subject = "whatever" - self.certificates = TLSCertificatesRequiresV1(self, "certificates") - self.framework.observe(self.on.install, self._on_install) - self.framework.observe( - self.on.certificates_relation_joined, self._on_certificates_relation_joined - ) - self.framework.observe( - self.certificates.on.certificate_available, self._on_certificate_available - ) - self.framework.observe( - self.certificates.on.certificate_expiring, self._on_certificate_expiring - ) - self.framework.observe( - self.certificates.on.certificate_revoked, self._on_certificate_revoked - ) - - def _on_install(self, event) -> None: - private_key_password = b"banana" - private_key = generate_private_key(password=private_key_password) - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update( - {"private_key_password": "banana", "private_key": private_key.decode()} - ) - - def _on_certificates_relation_joined(self, event: RelationJoinedEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - replicas_relation.data[self.app].update({"csr": csr.decode()}) - self.certificates.request_certificate_creation(certificate_signing_request=csr) - - def _on_certificate_available(self, event: CertificateAvailableEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update({"certificate": event.certificate}) - replicas_relation.data[self.app].update({"ca": event.ca}) - replicas_relation.data[self.app].update({"chain": event.chain}) - self.unit.status = ActiveStatus() - - def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - old_csr = replicas_relation.data[self.app].get("csr") - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - new_csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - self.certificates.request_certificate_renewal( - old_certificate_signing_request=old_csr, - new_certificate_signing_request=new_csr, - ) - replicas_relation.data[self.app].update({"csr": new_csr.decode()}) - - def _on_certificate_revoked(self, event: CertificateRevokedEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - old_csr = replicas_relation.data[self.app].get("csr") - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - new_csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - self.certificates.request_certificate_renewal( - old_certificate_signing_request=old_csr, - new_certificate_signing_request=new_csr, - ) - replicas_relation.data[self.app].update({"csr": new_csr.decode()}) - replicas_relation.data[self.app].pop("certificate") - replicas_relation.data[self.app].pop("ca") - replicas_relation.data[self.app].pop("chain") - self.unit.status = WaitingStatus("Waiting for new certificate") - - -if __name__ == "__main__": - main(ExampleRequirerCharm) -``` -""" # noqa: D405, D410, D411, D214, D416 - -import copy -import json -import logging -import uuid -from datetime import datetime, timedelta -from ipaddress import IPv4Address -from typing import Dict, List, Optional - -from cryptography import x509 -from cryptography.hazmat._oid import ExtensionOID -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.hazmat.primitives.serialization import pkcs12 -from cryptography.x509.extensions import Extension, ExtensionNotFound -from jsonschema import exceptions, validate # type: ignore[import] -from ops.charm import CharmBase, CharmEvents, RelationChangedEvent, UpdateStatusEvent -from ops.framework import EventBase, EventSource, Handle, Object - -# The unique Charmhub library identifier, never change it -LIBID = "afd8c2bccf834997afce12c2706d2ede" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 12 - - -REQUIRER_JSON_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/requirer.json", # noqa: E501 - "type": "object", - "title": "`tls_certificates` requirer root schema", - "description": "The `tls_certificates` root schema comprises the entire requirer databag for this interface.", # noqa: E501 - "examples": [ - { - "certificate_signing_requests": [ - { - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\\n-----END CERTIFICATE REQUEST-----\\n" # noqa: E501 - }, - { - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBAMk3raaX803cHvzlBF9LC7KORT46z4VjyU5PIaMb\\nQLIDgYKFYI0n5hf2Ra4FAHvOvEmW7bjNlHORFEmvnpcU5kPMNUyKFMTaC8LGmN8z\\nUBH3aK+0+FRvY4afn9tgj5435WqOG9QdoDJ0TJkjJbJI9M70UOgL711oU7ql6HxU\\n4d2ydFK9xAHrBwziNHgNZ72L95s4gLTXf0fAHYf15mDA9U5yc+YDubCKgTXzVySQ\\nUx73VCJLfC/XkZIh559IrnRv5G9fu6BMLEuBwAz6QAO4+/XidbKWN4r2XSq5qX4n\\n6EPQQWP8/nd4myq1kbg6Q8w68L/0YdfjCmbyf2TuoWeImdUCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQBIdwraBvpYo/rl5MH1+1Um6HRg4gOdQPY5WcJy9B9tgzJz\\nittRSlRGTnhyIo6fHgq9KHrmUthNe8mMTDailKFeaqkVNVvk7l0d1/B90Kz6OfmD\\nxN0qjW53oP7y3QB5FFBM8DjqjmUnz5UePKoX4AKkDyrKWxMwGX5RoET8c/y0y9jp\\nvSq3Wh5UpaZdWbe1oVY8CqMVUEVQL2DPjtopxXFz2qACwsXkQZxWmjvZnRiP8nP8\\nbdFaEuh9Q6rZ2QdZDEtrU4AodPU3NaukFr5KlTUQt3w/cl+5//zils6G5zUWJ2pN\\ng7+t9PTvXHRkH+LnwaVnmsBFU2e05qADQbfIn7JA\\n-----END CERTIFICATE REQUEST-----\\n" # noqa: E501 - }, - ] - } - ], - "properties": { - "certificate_signing_requests": { - "type": "array", - "items": { - "type": "object", - "properties": {"certificate_signing_request": {"type": "string"}}, - "required": ["certificate_signing_request"], - }, - } - }, - "required": ["certificate_signing_requests"], - "additionalProperties": True, -} - -PROVIDER_JSON_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/provider.json", # noqa: E501 - "type": "object", - "title": "`tls_certificates` provider root schema", - "description": "The `tls_certificates` root schema comprises the entire provider databag for this interface.", # noqa: E501 - "examples": [ - { - "certificates": [ - { - "ca": "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n", # noqa: E501 - "chain": [ - "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n" # noqa: E501, W505 - ], - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\n-----END CERTIFICATE REQUEST-----\n", # noqa: E501 - "certificate": "-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n", # noqa: E501 - } - ] - }, - { - "certificates": [ - { - "ca": "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n", # noqa: E501 - "chain": [ - "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n" # noqa: E501, W505 - ], - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\n-----END CERTIFICATE REQUEST-----\n", # noqa: E501 - "certificate": "-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n", # noqa: E501 - "revoked": True, - } - ] - }, - ], - "properties": { - "certificates": { - "$id": "#/properties/certificates", - "type": "array", - "items": { - "$id": "#/properties/certificates/items", - "type": "object", - "required": ["certificate_signing_request", "certificate", "ca", "chain"], - "properties": { - "certificate_signing_request": { - "$id": "#/properties/certificates/items/certificate_signing_request", - "type": "string", - }, - "certificate": { - "$id": "#/properties/certificates/items/certificate", - "type": "string", - }, - "ca": {"$id": "#/properties/certificates/items/ca", "type": "string"}, - "chain": { - "$id": "#/properties/certificates/items/chain", - "type": "array", - "items": { - "type": "string", - "$id": "#/properties/certificates/items/chain/items", - }, - }, - "revoked": { - "$id": "#/properties/certificates/items/revoked", - "type": "boolean", - }, - }, - "additionalProperties": True, - }, - } - }, - "required": ["certificates"], - "additionalProperties": True, -} - - -logger = logging.getLogger(__name__) - - -class CertificateAvailableEvent(EventBase): - """Charm Event triggered when a TLS certificate is available.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - - -class CertificateExpiringEvent(EventBase): - """Charm Event triggered when a TLS certificate is almost expired.""" - - def __init__(self, handle, certificate: str, expiry: str): - """CertificateExpiringEvent. - - Args: - handle (Handle): Juju framework handle - certificate (str): TLS Certificate - expiry (str): Datetime string reprensenting the time at which the certificate - won't be valid anymore. - """ - super().__init__(handle) - self.certificate = certificate - self.expiry = expiry - - def snapshot(self) -> dict: - """Returns snapshot.""" - return {"certificate": self.certificate, "expiry": self.expiry} - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.expiry = snapshot["expiry"] - - -class CertificateExpiredEvent(EventBase): - """Charm Event triggered when a TLS certificate is expired.""" - - def __init__(self, handle: Handle, certificate: str): - super().__init__(handle) - self.certificate = certificate - - def snapshot(self) -> dict: - """Returns snapshot.""" - return {"certificate": self.certificate} - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - - -class CertificateRevokedEvent(EventBase): - """Charm Event triggered when a TLS certificate is revoked.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - revoked: bool, - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - self.revoked = revoked - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - "revoked": self.revoked, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - self.revoked = snapshot["revoked"] - - -class CertificateCreationRequestEvent(EventBase): - """Charm Event triggered when a TLS certificate is required.""" - - def __init__(self, handle: Handle, certificate_signing_request: str, relation_id: int): - super().__init__(handle) - self.certificate_signing_request = certificate_signing_request - self.relation_id = relation_id - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate_signing_request": self.certificate_signing_request, - "relation_id": self.relation_id, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.relation_id = snapshot["relation_id"] - - -class CertificateRevocationRequestEvent(EventBase): - """Charm Event triggered when a TLS certificate needs to be revoked.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: str, - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - - -def _load_relation_data(raw_relation_data: dict) -> dict: - """Loads relation data from the relation data bag. - - Json loads all data. - - Args: - raw_relation_data: Relation data from the databag - - Returns: - dict: Relation data in dict format. - """ - certificate_data = dict() - for key in raw_relation_data: - try: - certificate_data[key] = json.loads(raw_relation_data[key]) - except (json.decoder.JSONDecodeError, TypeError): - certificate_data[key] = raw_relation_data[key] - return certificate_data - - -def generate_ca( - private_key: bytes, - subject: str, - private_key_password: Optional[bytes] = None, - validity: int = 365, - country: str = "US", -) -> bytes: - """Generates a CA Certificate. - - Args: - private_key (bytes): Private key - subject (str): Certificate subject - private_key_password (bytes): Private key password - validity (int): Certificate validity time (in days) - country (str): Certificate Issuing country - - Returns: - bytes: CA Certificate. - """ - private_key_object = serialization.load_pem_private_key( - private_key, password=private_key_password - ) - subject = issuer = x509.Name( - [ - x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country), - x509.NameAttribute(x509.NameOID.COMMON_NAME, subject), - ] - ) - subject_identifier_object = x509.SubjectKeyIdentifier.from_public_key( - private_key_object.public_key() # type: ignore[arg-type] - ) - subject_identifier = key_identifier = subject_identifier_object.public_bytes() - cert = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(private_key_object.public_key()) # type: ignore[arg-type] - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.utcnow()) - .not_valid_after(datetime.utcnow() + timedelta(days=validity)) - .add_extension(x509.SubjectKeyIdentifier(digest=subject_identifier), critical=False) - .add_extension( - x509.AuthorityKeyIdentifier( - key_identifier=key_identifier, - authority_cert_issuer=None, - authority_cert_serial_number=None, - ), - critical=False, - ) - .add_extension( - x509.BasicConstraints(ca=True, path_length=None), - critical=True, - ) - .sign(private_key_object, hashes.SHA256()) # type: ignore[arg-type] - ) - return cert.public_bytes(serialization.Encoding.PEM) - - -def generate_certificate( - csr: bytes, - ca: bytes, - ca_key: bytes, - ca_key_password: Optional[bytes] = None, - validity: int = 365, - alt_names: Optional[List[str]] = None, -) -> bytes: - """Generates a TLS certificate based on a CSR. - - Args: - csr (bytes): CSR - ca (bytes): CA Certificate - ca_key (bytes): CA private key - ca_key_password: CA private key password - validity (int): Certificate validity (in days) - alt_names (list): List of alt names to put on cert - prefer putting SANs in CSR - - Returns: - bytes: Certificate - """ - csr_object = x509.load_pem_x509_csr(csr) - subject = csr_object.subject - issuer = x509.load_pem_x509_certificate(ca).issuer - private_key = serialization.load_pem_private_key(ca_key, password=ca_key_password) - - certificate_builder = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(csr_object.public_key()) - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.utcnow()) - .not_valid_after(datetime.utcnow() + timedelta(days=validity)) - ) - - extensions_list = csr_object.extensions - san_ext: Optional[x509.Extension] = None - if alt_names: - full_sans_dns = alt_names.copy() - try: - loaded_san_ext = csr_object.extensions.get_extension_for_class( - x509.SubjectAlternativeName - ) - full_sans_dns.extend(loaded_san_ext.value.get_values_for_type(x509.DNSName)) - except ExtensionNotFound: - pass - finally: - san_ext = Extension( - ExtensionOID.SUBJECT_ALTERNATIVE_NAME, - False, - x509.SubjectAlternativeName([x509.DNSName(name) for name in full_sans_dns]), - ) - if not extensions_list: - extensions_list = x509.Extensions([san_ext]) - - for extension in extensions_list: - if extension.value.oid == ExtensionOID.SUBJECT_ALTERNATIVE_NAME and san_ext: - extension = san_ext - - certificate_builder = certificate_builder.add_extension( - extension.value, - critical=extension.critical, - ) - certificate_builder._version = x509.Version.v3 - cert = certificate_builder.sign(private_key, hashes.SHA256()) # type: ignore[arg-type] - return cert.public_bytes(serialization.Encoding.PEM) - - -def generate_pfx_package( - certificate: bytes, - private_key: bytes, - package_password: str, - private_key_password: Optional[bytes] = None, -) -> bytes: - """Generates a PFX package to contain the TLS certificate and private key. - - Args: - certificate (bytes): TLS certificate - private_key (bytes): Private key - package_password (str): Password to open the PFX package - private_key_password (bytes): Private key password - - Returns: - bytes: - """ - private_key_object = serialization.load_pem_private_key( - private_key, password=private_key_password - ) - certificate_object = x509.load_pem_x509_certificate(certificate) - name = certificate_object.subject.rfc4514_string() - pfx_bytes = pkcs12.serialize_key_and_certificates( - name=name.encode(), - cert=certificate_object, - key=private_key_object, # type: ignore[arg-type] - cas=None, - encryption_algorithm=serialization.BestAvailableEncryption(package_password.encode()), - ) - return pfx_bytes - - -def generate_private_key( - password: Optional[bytes] = None, - key_size: int = 2048, - public_exponent: int = 65537, -) -> bytes: - """Generates a private key. - - Args: - password (bytes): Password for decrypting the private key - key_size (int): Key size in bytes - public_exponent: Public exponent. - - Returns: - bytes: Private Key - """ - private_key = rsa.generate_private_key( - public_exponent=public_exponent, - key_size=key_size, - ) - key_bytes = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.BestAvailableEncryption(password) - if password - else serialization.NoEncryption(), - ) - return key_bytes - - -def generate_csr( - private_key: bytes, - subject: str, - add_unique_id_to_subject_name: bool = True, - organization: Optional[str] = None, - email_address: Optional[str] = None, - country_name: Optional[str] = None, - private_key_password: Optional[bytes] = None, - sans: Optional[List[str]] = None, - sans_oid: Optional[List[str]] = None, - sans_ip: Optional[List[str]] = None, - sans_dns: Optional[List[str]] = None, - additional_critical_extensions: Optional[List] = None, -) -> bytes: - """Generates a CSR using private key and subject. - - Args: - private_key (bytes): Private key - subject (str): CSR Subject. - add_unique_id_to_subject_name (bool): Whether a unique ID must be added to the CSR's - subject name. Always leave to "True" when the CSR is used to request certificates - using the tls-certificates relation. - organization (str): Name of organization. - email_address (str): Email address. - country_name (str): Country Name. - private_key_password (bytes): Private key password - sans (list): Use sans_dns - this will be deprecated in a future release - List of DNS subject alternative names (keeping it for now for backward compatibility) - sans_oid (list): List of registered ID SANs - sans_dns (list): List of DNS subject alternative names (similar to the arg: sans) - sans_ip (list): List of IP subject alternative names - additional_critical_extensions (list): List if critical additional extension objects. - Object must be a x509 ExtensionType. - - Returns: - bytes: CSR - """ - signing_key = serialization.load_pem_private_key(private_key, password=private_key_password) - subject_name = [x509.NameAttribute(x509.NameOID.COMMON_NAME, subject)] - if add_unique_id_to_subject_name: - unique_identifier = uuid.uuid4() - subject_name.append( - x509.NameAttribute(x509.NameOID.X500_UNIQUE_IDENTIFIER, str(unique_identifier)) - ) - if organization: - subject_name.append(x509.NameAttribute(x509.NameOID.ORGANIZATION_NAME, organization)) - if email_address: - subject_name.append(x509.NameAttribute(x509.NameOID.EMAIL_ADDRESS, email_address)) - if country_name: - subject_name.append(x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country_name)) - csr = x509.CertificateSigningRequestBuilder(subject_name=x509.Name(subject_name)) - - _sans: List[x509.GeneralName] = [] - if sans_oid: - _sans.extend([x509.RegisteredID(x509.ObjectIdentifier(san)) for san in sans_oid]) - if sans_ip: - _sans.extend([x509.IPAddress(IPv4Address(san)) for san in sans_ip]) - if sans: - _sans.extend([x509.DNSName(san) for san in sans]) - if sans_dns: - _sans.extend([x509.DNSName(san) for san in sans_dns]) - if _sans: - csr = csr.add_extension(x509.SubjectAlternativeName(set(_sans)), critical=False) - - if additional_critical_extensions: - for extension in additional_critical_extensions: - csr = csr.add_extension(extension, critical=True) - - signed_certificate = csr.sign(signing_key, hashes.SHA256()) # type: ignore[arg-type] - return signed_certificate.public_bytes(serialization.Encoding.PEM) - - -class CertificatesProviderCharmEvents(CharmEvents): - """List of events that the TLS Certificates provider charm can leverage.""" - - certificate_creation_request = EventSource(CertificateCreationRequestEvent) - certificate_revocation_request = EventSource(CertificateRevocationRequestEvent) - - -class CertificatesRequirerCharmEvents(CharmEvents): - """List of events that the TLS Certificates requirer charm can leverage.""" - - certificate_available = EventSource(CertificateAvailableEvent) - certificate_expiring = EventSource(CertificateExpiringEvent) - certificate_expired = EventSource(CertificateExpiredEvent) - certificate_revoked = EventSource(CertificateRevokedEvent) - - -class TLSCertificatesProvidesV1(Object): - """TLS certificates provider class to be instantiated by TLS certificates providers.""" - - on = CertificatesProviderCharmEvents() - - def __init__(self, charm: CharmBase, relationship_name: str): - super().__init__(charm, relationship_name) - self.framework.observe( - charm.on[relationship_name].relation_changed, self._on_relation_changed - ) - self.charm = charm - self.relationship_name = relationship_name - - def _add_certificate( - self, - relation_id: int, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - ) -> None: - """Adds certificate to relation data. - - Args: - relation_id (int): Relation id - certificate (str): Certificate - certificate_signing_request (str): Certificate Signing Request - ca (str): CA Certificate - chain (list): CA Chain - - Returns: - None - """ - relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - new_certificate = { - "certificate": certificate, - "certificate_signing_request": certificate_signing_request, - "ca": ca, - "chain": chain, - } - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = provider_relation_data.get("certificates", []) - certificates = copy.deepcopy(provider_certificates) - if new_certificate in certificates: - logger.info("Certificate already in relation data - Doing nothing") - return - certificates.append(new_certificate) - relation.data[self.model.app]["certificates"] = json.dumps(certificates) - - def _remove_certificate( - self, - relation_id: int, - certificate: Optional[str] = None, - certificate_signing_request: Optional[str] = None, - ) -> None: - """Removes certificate from a given relation based on user provided certificate or csr. - - Args: - relation_id (int): Relation id - certificate (str): Certificate (optional) - certificate_signing_request: Certificate signing request (optional) - - Returns: - None - """ - relation = self.model.get_relation( - relation_name=self.relationship_name, - relation_id=relation_id, - ) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} with relation id {relation_id} does not exist" - ) - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = provider_relation_data.get("certificates", []) - certificates = copy.deepcopy(provider_certificates) - for certificate_dict in certificates: - if certificate and certificate_dict["certificate"] == certificate: - certificates.remove(certificate_dict) - if ( - certificate_signing_request - and certificate_dict["certificate_signing_request"] == certificate_signing_request - ): - certificates.remove(certificate_dict) - relation.data[self.model.app]["certificates"] = json.dumps(certificates) - - @staticmethod - def _relation_data_is_valid(certificates_data: dict) -> bool: - """Uses JSON schema validator to validate relation data content. - - Args: - certificates_data (dict): Certificate data dictionary as retrieved from relation data. - - Returns: - bool: True/False depending on whether the relation data follows the json schema. - """ - try: - validate(instance=certificates_data, schema=REQUIRER_JSON_SCHEMA) - return True - except exceptions.ValidationError: - return False - - def revoke_all_certificates(self) -> None: - """Revokes all certificates of this provider. - - This method is meant to be used when the Root CA has changed. - """ - for relation in self.model.relations[self.relationship_name]: - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = copy.deepcopy(provider_relation_data.get("certificates", [])) - for certificate in provider_certificates: - certificate["revoked"] = True - relation.data[self.model.app]["certificates"] = json.dumps(provider_certificates) - - def set_relation_certificate( - self, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - relation_id: int, - ) -> None: - """Adds certificates to relation data. - - Args: - certificate (str): Certificate - certificate_signing_request (str): Certificate signing request - ca (str): CA Certificate - chain (list): CA Chain - relation_id (int): Juju relation ID - - Returns: - None - """ - certificates_relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - self._remove_certificate( - certificate_signing_request=certificate_signing_request.strip(), - relation_id=relation_id, - ) - self._add_certificate( - relation_id=relation_id, - certificate=certificate.strip(), - certificate_signing_request=certificate_signing_request.strip(), - ca=ca.strip(), - chain=[cert.strip() for cert in chain], - ) - - def remove_certificate(self, certificate: str) -> None: - """Removes a given certificate from relation data. - - Args: - certificate (str): TLS Certificate - - Returns: - None - """ - certificates_relation = self.model.relations[self.relationship_name] - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - for certificate_relation in certificates_relation: - self._remove_certificate(certificate=certificate, relation_id=certificate_relation.id) - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handler triggerred on relation changed event. - - Looks at the relation data and either emits: - - certificate request event: If the unit relation data contains a CSR for which - a certificate does not exist in the provider relation data. - - certificate revocation event: If the provider relation data contains a CSR for which - a csr does not exist in the requirer relation data. - - Args: - event: Juju event - - Returns: - None - """ - assert event.unit is not None - requirer_relation_data = _load_relation_data(event.relation.data[event.unit]) - provider_relation_data = _load_relation_data(event.relation.data[self.charm.app]) - if not self._relation_data_is_valid(requirer_relation_data): - logger.warning( - f"Relation data did not pass JSON Schema validation: {requirer_relation_data}" - ) - return - provider_certificates = provider_relation_data.get("certificates", []) - requirer_csrs = requirer_relation_data.get("certificate_signing_requests", []) - provider_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in provider_certificates - ] - requirer_unit_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in requirer_csrs - ] - for certificate_signing_request in requirer_unit_csrs: - if certificate_signing_request not in provider_csrs: - self.on.certificate_creation_request.emit( - certificate_signing_request=certificate_signing_request, - relation_id=event.relation.id, - ) - self._revoke_certificates_for_which_no_csr_exists(relation_id=event.relation.id) - - def _revoke_certificates_for_which_no_csr_exists(self, relation_id: int) -> None: - """Revokes certificates for which no unit has a CSR. - - Goes through all generated certificates and compare agains the list of CSRS for all units - of a given relationship. - - Args: - relation_id (int): Relation id - - Returns: - None - """ - certificates_relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - provider_relation_data = _load_relation_data(certificates_relation.data[self.charm.app]) - list_of_csrs: List[str] = [] - for unit in certificates_relation.units: - requirer_relation_data = _load_relation_data(certificates_relation.data[unit]) - requirer_csrs = requirer_relation_data.get("certificate_signing_requests", []) - list_of_csrs.extend(csr["certificate_signing_request"] for csr in requirer_csrs) - provider_certificates = provider_relation_data.get("certificates", []) - for certificate in provider_certificates: - if certificate["certificate_signing_request"] not in list_of_csrs: - self.on.certificate_revocation_request.emit( - certificate=certificate["certificate"], - certificate_signing_request=certificate["certificate_signing_request"], - ca=certificate["ca"], - chain=certificate["chain"], - ) - self.remove_certificate(certificate=certificate["certificate"]) - - -class TLSCertificatesRequiresV1(Object): - """TLS certificates requirer class to be instantiated by TLS certificates requirers.""" - - on = CertificatesRequirerCharmEvents() - - def __init__( - self, - charm: CharmBase, - relationship_name: str, - expiry_notification_time: int = 168, - ): - """Generates/use private key and observes relation changed event. - - Args: - charm: Charm object - relationship_name: Juju relation name - expiry_notification_time (int): Time difference between now and expiry (in hours). - Used to trigger the CertificateExpiring event. Default: 7 days. - """ - super().__init__(charm, relationship_name) - self.relationship_name = relationship_name - self.charm = charm - self.expiry_notification_time = expiry_notification_time - self.framework.observe( - charm.on[relationship_name].relation_changed, self._on_relation_changed - ) - self.framework.observe(charm.on.update_status, self._on_update_status) - - @property - def _requirer_csrs(self) -> List[Dict[str, str]]: - """Returns list of requirer CSR's from relation data.""" - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - requirer_relation_data = _load_relation_data(relation.data[self.model.unit]) - return requirer_relation_data.get("certificate_signing_requests", []) - - @property - def _provider_certificates(self) -> List[Dict[str, str]]: - """Returns list of provider CSR's from relation data.""" - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - if not relation.app: - raise RuntimeError(f"Remote app for relation {self.relationship_name} does not exist") - provider_relation_data = _load_relation_data(relation.data[relation.app]) - return provider_relation_data.get("certificates", []) - - def _add_requirer_csr(self, csr: str) -> None: - """Adds CSR to relation data. - - Args: - csr (str): Certificate Signing Request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - new_csr_dict = {"certificate_signing_request": csr} - if new_csr_dict in self._requirer_csrs: - logger.info("CSR already in relation data - Doing nothing") - return - requirer_csrs = copy.deepcopy(self._requirer_csrs) - requirer_csrs.append(new_csr_dict) - relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs) - - def _remove_requirer_csr(self, csr: str) -> None: - """Removes CSR from relation data. - - Args: - csr (str): Certificate signing request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - requirer_csrs = copy.deepcopy(self._requirer_csrs) - csr_dict = {"certificate_signing_request": csr} - if csr_dict not in requirer_csrs: - logger.info("CSR not in relation data - Doing nothing") - return - requirer_csrs.remove(csr_dict) - relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs) - - def request_certificate_creation(self, certificate_signing_request: bytes) -> None: - """Request TLS certificate to provider charm. - - Args: - certificate_signing_request (bytes): Certificate Signing Request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - message = ( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - logger.error(message) - raise RuntimeError(message) - self._add_requirer_csr(certificate_signing_request.decode().strip()) - logger.info("Certificate request sent to provider") - - def request_certificate_revocation(self, certificate_signing_request: bytes) -> None: - """Removes CSR from relation data. - - The provider of this relation is then expected to remove certificates associated to this - CSR from the relation data as well and emit a request_certificate_revocation event for the - provider charm to interpret. - - Args: - certificate_signing_request (bytes): Certificate Signing Request - - Returns: - None - """ - self._remove_requirer_csr(certificate_signing_request.decode().strip()) - logger.info("Certificate revocation sent to provider") - - def request_certificate_renewal( - self, old_certificate_signing_request: bytes, new_certificate_signing_request: bytes - ) -> None: - """Renews certificate. - - Removes old CSR from relation data and adds new one. - - Args: - old_certificate_signing_request: Old CSR - new_certificate_signing_request: New CSR - - Returns: - None - """ - try: - self.request_certificate_revocation( - certificate_signing_request=old_certificate_signing_request - ) - except RuntimeError: - logger.warning("Certificate revocation failed.") - self.request_certificate_creation( - certificate_signing_request=new_certificate_signing_request - ) - logger.info("Certificate renewal request completed.") - - @staticmethod - def _relation_data_is_valid(certificates_data: dict) -> bool: - """Checks whether relation data is valid based on json schema. - - Args: - certificates_data: Certificate data in dict format. - - Returns: - bool: Whether relation data is valid. - """ - try: - validate(instance=certificates_data, schema=PROVIDER_JSON_SCHEMA) - return True - except exceptions.ValidationError: - return False - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handler triggered on relation changed events. - - Args: - event: Juju event - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - logger.warning(f"No relation: {self.relationship_name}") - return - if not relation.app: - logger.warning(f"No remote app in relation: {self.relationship_name}") - return - provider_relation_data = _load_relation_data(relation.data[relation.app]) - if not self._relation_data_is_valid(provider_relation_data): - logger.warning( - f"Provider relation data did not pass JSON Schema validation: " - f"{event.relation.data[relation.app]}" - ) - return - requirer_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in self._requirer_csrs - ] - for certificate in self._provider_certificates: - if certificate["certificate_signing_request"] in requirer_csrs: - if certificate.get("revoked", False): - self.on.certificate_revoked.emit( - certificate_signing_request=certificate["certificate_signing_request"], - certificate=certificate["certificate"], - ca=certificate["ca"], - chain=certificate["chain"], - revoked=True, - ) - else: - self.on.certificate_available.emit( - certificate_signing_request=certificate["certificate_signing_request"], - certificate=certificate["certificate"], - ca=certificate["ca"], - chain=certificate["chain"], - ) - - def _on_update_status(self, event: UpdateStatusEvent) -> None: - """Triggered on update status event. - - Goes through each certificate in the "certificates" relation and checks their expiry date. - If they are close to expire (<7 days), emits a CertificateExpiringEvent event and if - they are expired, emits a CertificateExpiredEvent. - - Args: - event (UpdateStatusEvent): Juju event - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - logger.warning(f"No relation: {self.relationship_name}") - return - if not relation.app: - logger.warning(f"No remote app in relation: {self.relationship_name}") - return - provider_relation_data = _load_relation_data(relation.data[relation.app]) - if not self._relation_data_is_valid(provider_relation_data): - logger.warning( - f"Provider relation data did not pass JSON Schema validation: " - f"{relation.data[relation.app]}" - ) - return - for certificate_dict in self._provider_certificates: - certificate = certificate_dict["certificate"] - try: - certificate_object = x509.load_pem_x509_certificate(data=certificate.encode()) - except ValueError: - logger.warning("Could not load certificate.") - continue - time_difference = certificate_object.not_valid_after - datetime.utcnow() - if time_difference.total_seconds() < 0: - logger.warning("Certificate is expired") - self.on.certificate_expired.emit(certificate=certificate) - self.request_certificate_revocation(certificate.encode()) - continue - if time_difference.total_seconds() < (self.expiry_notification_time * 60 * 60): - logger.warning("Certificate almost expired") - self.on.certificate_expiring.emit( - certificate=certificate, expiry=certificate_object.not_valid_after.isoformat() - ) diff --git a/charms/octavia-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/octavia-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/octavia-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/octavia-k8s/osci.yaml b/charms/octavia-k8s/osci.yaml deleted file mode 100644 index 0cb2ddd6..00000000 --- a/charms/octavia-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: octavia-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/octavia-k8s/pyproject.toml b/charms/octavia-k8s/pyproject.toml deleted file mode 100644 index 30821404..00000000 --- a/charms/octavia-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/octavia-k8s/rename.sh b/charms/octavia-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/octavia-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/octavia-k8s/requirements.txt b/charms/octavia-k8s/requirements.txt index 230257ac..db47310f 100644 --- a/charms/octavia-k8s/requirements.txt +++ b/charms/octavia-k8s/requirements.txt @@ -3,5 +3,7 @@ ops jinja2 jsonschema pydantic<2.0 -git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam lightkube + +# From ops_sunbeam +tenacity diff --git a/charms/octavia-k8s/src/templates/parts/database-connection b/charms/octavia-k8s/src/templates/parts/database-connection deleted file mode 100644 index 1fd70ce2..00000000 --- a/charms/octavia-k8s/src/templates/parts/database-connection +++ /dev/null @@ -1,3 +0,0 @@ -{% if database.connection -%} -connection = {{ database.connection }} -{% endif -%} diff --git a/charms/octavia-k8s/src/templates/parts/identity-data b/charms/octavia-k8s/src/templates/parts/identity-data deleted file mode 100644 index 574c3248..00000000 --- a/charms/octavia-k8s/src/templates/parts/identity-data +++ /dev/null @@ -1,21 +0,0 @@ -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} diff --git a/charms/octavia-k8s/src/templates/parts/section-database b/charms/octavia-k8s/src/templates/parts/section-database deleted file mode 100644 index 986d9b10..00000000 --- a/charms/octavia-k8s/src/templates/parts/section-database +++ /dev/null @@ -1,3 +0,0 @@ -[database] -{% include "parts/database-connection" %} -connection_recycle_time = 200 diff --git a/charms/octavia-k8s/src/templates/parts/section-federation b/charms/octavia-k8s/src/templates/parts/section-federation deleted file mode 100644 index 65ee99ed..00000000 --- a/charms/octavia-k8s/src/templates/parts/section-federation +++ /dev/null @@ -1,10 +0,0 @@ -{% if trusted_dashboards %} -[federation] -{% for dashboard_url in trusted_dashboards -%} -trusted_dashboard = {{ dashboard_url }} -{% endfor -%} -{% endif %} -{% for sp in fid_sps -%} -[{{ sp['protocol-name'] }}] -remote_id_attribute = {{ sp['remote-id-attribute'] }} -{% endfor -%} diff --git a/charms/octavia-k8s/src/templates/parts/section-identity b/charms/octavia-k8s/src/templates/parts/section-identity deleted file mode 100644 index 2cf792eb..00000000 --- a/charms/octavia-k8s/src/templates/parts/section-identity +++ /dev/null @@ -1,3 +0,0 @@ -[keystone_authtoken] -{% include "parts/identity-data" %} -{% include "parts/service-token" %} diff --git a/charms/octavia-k8s/src/templates/parts/section-middleware b/charms/octavia-k8s/src/templates/parts/section-middleware deleted file mode 100644 index e65f1d98..00000000 --- a/charms/octavia-k8s/src/templates/parts/section-middleware +++ /dev/null @@ -1,6 +0,0 @@ -{% for section in sections -%} -[{{section}}] -{% for key, value in sections[section].items() -%} -{{ key }} = {{ value }} -{% endfor %} -{%- endfor %} diff --git a/charms/octavia-k8s/src/templates/parts/section-signing b/charms/octavia-k8s/src/templates/parts/section-signing deleted file mode 100644 index cb7d69ae..00000000 --- a/charms/octavia-k8s/src/templates/parts/section-signing +++ /dev/null @@ -1,15 +0,0 @@ -{% if enable_signing -%} -[signing] -{% if certfile -%} -certfile = {{ certfile }} -{% endif -%} -{% if keyfile -%} -keyfile = {{ keyfile }} -{% endif -%} -{% if ca_certs -%} -ca_certs = {{ ca_certs }} -{% endif -%} -{% if ca_key -%} -ca_key = {{ ca_key }} -{% endif -%} -{% endif -%} \ No newline at end of file diff --git a/charms/octavia-k8s/src/templates/parts/service-token b/charms/octavia-k8s/src/templates/parts/service-token deleted file mode 100644 index 51c0073f..00000000 --- a/charms/octavia-k8s/src/templates/parts/service-token +++ /dev/null @@ -1,2 +0,0 @@ -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True diff --git a/charms/octavia-k8s/test-requirements.txt b/charms/octavia-k8s/test-requirements.txt deleted file mode 100644 index d1a61d34..00000000 --- a/charms/octavia-k8s/test-requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -# This file is managed centrally. If you find the need to modify this as a -# one-off, please don't. Intead, consult #openstack-charms and ask about -# requirements management in charms via bot-control. Thank you. - -coverage -mock -flake8 -stestr -ops diff --git a/charms/octavia-k8s/tests/unit/test_charm.py b/charms/octavia-k8s/tests/unit/test_charm.py index e84092d4..a4b7f6e1 100644 --- a/charms/octavia-k8s/tests/unit/test_charm.py +++ b/charms/octavia-k8s/tests/unit/test_charm.py @@ -16,9 +16,8 @@ """Unit tests for Octavia operator.""" -import ops_sunbeam.test_utils as test_utils - import charm +import ops_sunbeam.test_utils as test_utils class _OctaviaOVNOperatorCharm(charm.OctaviaOVNOperatorCharm): diff --git a/charms/octavia-k8s/tox.ini b/charms/octavia-k8s/tox.ini deleted file mode 100644 index 067963f9..00000000 --- a/charms/octavia-k8s/tox.ini +++ /dev/null @@ -1,165 +0,0 @@ -# Operator charm (with zaza): tox.ini - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - HOME - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/openstack-exporter-k8s/.gitignore b/charms/openstack-exporter-k8s/.gitignore deleted file mode 100644 index 24ff2e41..00000000 --- a/charms/openstack-exporter-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -*.charm -.tox/ -.coverage -__pycache__/ -*.py[cod] -.idea -.vscode/ -*.swp -.stestr/ diff --git a/charms/openstack-exporter-k8s/.gitreview b/charms/openstack-exporter-k8s/.gitreview deleted file mode 100644 index 9859257a..00000000 --- a/charms/openstack-exporter-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-openstack-exporter-k8s.git -defaultbranch=main diff --git a/charms/openstack-exporter-k8s/.stestr.conf b/charms/openstack-exporter-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/openstack-exporter-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/openstack-exporter-k8s/.zuul.yaml b/charms/openstack-exporter-k8s/.zuul.yaml deleted file mode 100644 index 5748d306..00000000 --- a/charms/openstack-exporter-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: openstack-exporter-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/openstack-exporter-k8s/charmcraft.yaml b/charms/openstack-exporter-k8s/charmcraft.yaml index 2fdc318d..9556149e 100644 --- a/charms/openstack-exporter-k8s/charmcraft.yaml +++ b/charms/openstack-exporter-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/openstack-exporter-k8s/lib/charms/keystone_k8s/v0/identity_resource.py b/charms/openstack-exporter-k8s/lib/charms/keystone_k8s/v0/identity_resource.py deleted file mode 100644 index 1f10383a..00000000 --- a/charms/openstack-exporter-k8s/lib/charms/keystone_k8s/v0/identity_resource.py +++ /dev/null @@ -1,393 +0,0 @@ -"""IdentityResourceProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the identity_ops interface. - -Import `IdentityResourceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_ops" - -Also provide additional parameters to the charm object: - - request - -Three events are also available to respond to: - - provider_ready - - provider_goneaway - - response_avaialable - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v0.identity_resource import IdentityResourceRequires - -class IdentityResourceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityResource Requires - self.identity_resource = IdentityResourceRequires( - self, "identity_ops", - ) - self.framework.observe( - self.identity_resource.on.provider_ready, self._on_identity_resource_ready) - self.framework.observe( - self.identity_resource.on.provider_goneaway, self._on_identity_resource_goneaway) - self.framework.observe( - self.identity_resource.on.response_available, self._on_identity_resource_response) - - def _on_identity_resource_ready(self, event): - '''React to the IdentityResource provider_ready event. - - This event happens when n IdentityResource relation is added to the - model. Ready to send any ops to keystone. - ''' - # Ready to send any ops. - pass - - def _on_identity_resource_response(self, event): - '''React to the IdentityResource response_available event. - - The IdentityResource interface will provide the response for the ops sent. - ''' - # Read the response for the ops sent. - pass - - def _on_identity_resource_goneaway(self, event): - '''React to the IdentityResource goneaway event. - - This event happens when an IdentityResource relation is removed. - ''' - # IdentityResource Relation has goneaway. No ops can be sent. - pass -``` - -A sample ops request can be of format -{ - "id": - "tag": - "ops": [ - { - "name": , - "params": { - : , - : - } - } - ] -} - -For any sensitive data in the ops params, the charm can create secrets and pass -secret id instead of sensitive data as part of ops request. The charm should -ensure to grant secret access to provider charm i.e., keystone over relation. -The secret content should hold the sensitive data with same name as param name. -""" - -import json -import logging -from typing import ( - Optional, -) - -from ops.charm import ( - CharmBase, - RelationBrokenEvent, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import ( - EventBase, - EventSource, - Object, - ObjectEvents, - StoredState, -) -from ops.model import ( - Relation, -) - -logger = logging.getLogger(__name__) - - -# The unique Charmhub library identifier, never change it -LIBID = "b419d4d8249e423487daafc3665ed06f" - -# 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 - - -REQUEST_NOT_SENT = 1 -REQUEST_SENT = 2 -REQUEST_PROCESSED = 3 - - -class IdentityOpsProviderReadyEvent(RelationEvent): - """Has IdentityOpsProviderReady Event.""" - - pass - - -class IdentityOpsResponseEvent(RelationEvent): - """Has IdentityOpsResponse Event.""" - - pass - - -class IdentityOpsProviderGoneAwayEvent(RelationEvent): - """Has IdentityOpsProviderGoneAway Event.""" - - pass - - -class IdentityResourceResponseEvents(ObjectEvents): - """Events class for `on`.""" - - provider_ready = EventSource(IdentityOpsProviderReadyEvent) - response_available = EventSource(IdentityOpsResponseEvent) - provider_goneaway = EventSource(IdentityOpsProviderGoneAwayEvent) - - -class IdentityResourceRequires(Object): - """IdentityResourceRequires class.""" - - on = IdentityResourceResponseEvents() - _stored = StoredState() - - def __init__(self, charm: CharmBase, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self._stored.set_default(provider_ready=False, requests=[]) - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_resource_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_resource_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_resource_relation_broken, - ) - - def _on_identity_resource_relation_joined( - self, event: RelationJoinedEvent - ): - """Handle IdentityResource joined.""" - self._stored.provider_ready = True - self.on.provider_ready.emit(event.relation) - - def _on_identity_resource_relation_changed( - self, event: RelationChangedEvent - ): - """Handle IdentityResource changed.""" - id_ = self.response.get("id") - self.save_request_in_store(id_, None, None, REQUEST_PROCESSED) - self.on.response_available.emit(event.relation) - - def _on_identity_resource_relation_broken( - self, event: RelationBrokenEvent - ): - """Handle IdentityResource broken.""" - self._stored.provider_ready = False - self.on.provider_goneaway.emit(event.relation) - - @property - def _identity_resource_rel(self) -> Optional[Relation]: - """The IdentityResource relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def response(self) -> dict: - """Response object from keystone.""" - response = self.get_remote_app_data("response") - if not response: - return {} - - try: - return json.loads(response) - except Exception as e: - logger.debug(str(e)) - - return {} - - def save_request_in_store( - self, id: str, tag: str, ops: list, state: int - ) -> None: - """Save request in the store.""" - if id is None: - return - - for request in self._stored.requests: - if request.get("id") == id: - if tag: - request["tag"] = tag - if ops: - request["ops"] = ops - request["state"] = state - return - - # New request - self._stored.requests.append( - {"id": id, "tag": tag, "ops": ops, "state": state} - ) - - def get_request_from_store(self, id: str) -> dict: - """Get request from the stote.""" - for request in self._stored.requests: - if request.get("id") == id: - return request - - return {} - - def is_request_processed(self, id: str) -> bool: - """Check if request is processed.""" - for request in self._stored.requests: - if ( - request.get("id") == id - and request.get("state") == REQUEST_PROCESSED - ): - return True - - return False - - def get_remote_app_data(self, key: str) -> Optional[str]: - """Return the value for the given key from remote app data.""" - if self._identity_resource_rel: - data = self._identity_resource_rel.data[ - self._identity_resource_rel.app - ] - return data.get(key) - - return None - - def ready(self) -> bool: - """Interface is ready or not. - - Interface is considered ready if the op request is processed - and response is sent. In case of non leader unit, just consider - the interface is ready. - """ - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, set the interface to ready") - return True - - try: - app_data = self._identity_resource_rel.data[self.charm.app] - if "request" not in app_data: - return False - - request = json.loads(app_data["request"]) - request_id = request.get("id") - response_id = self.response.get("id") - if request_id == response_id: - return True - except Exception as e: - logger.debug(str(e)) - - return False - - def request_ops(self, request: dict) -> None: - """Request keystone ops.""" - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, not sending request") - return - - id_ = request.get("id") - tag = request.get("tag") - ops = request.get("ops") - req = self.get_request_from_store(id_) - if req and req.get("state") == REQUEST_PROCESSED: - logger.debug("Request {id_} already processed") - return - - if not self._stored.provider_ready: - self.save_request_in_store(id_, tag, ops, REQUEST_NOT_SENT) - logger.debug("Keystone not yet ready to take requests") - return - - logger.debug("Requesting ops to keystone") - app_data = self._identity_resource_rel.data[self.charm.app] - app_data["request"] = json.dumps(request) - self.save_request_in_store(id_, tag, ops, REQUEST_SENT) - - -class IdentityOpsRequestEvent(EventBase): - """Has IdentityOpsRequest Event.""" - - def __init__(self, handle, relation_id, relation_name, request): - """Initialise event.""" - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.request = request - - def snapshot(self): - """Snapshot the event.""" - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "request": self.request, - } - - def restore(self, snapshot): - """Restore the event.""" - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.request = snapshot["request"] - - -class IdentityResourceProviderEvents(ObjectEvents): - """Events class for `on`.""" - - process_op = EventSource(IdentityOpsRequestEvent) - - -class IdentityResourceProvides(Object): - """IdentityResourceProvides class.""" - - on = IdentityResourceProviderEvents() - - def __init__(self, charm: CharmBase, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_resource_relation_changed, - ) - - def _on_identity_resource_relation_changed( - self, event: RelationChangedEvent - ): - """Handle IdentityResource changed.""" - request = event.relation.data[event.relation.app].get("request") - if request is not None: - self.on.process_op.emit( - event.relation.id, event.relation.name, request - ) - - def set_ops_response( - self, relation_id: str, relation_name: str, ops_response: dict - ) -> None: - """Set response to ops request.""" - if not self.model.unit.is_leader(): - logger.debug("Not a leader unit, not sending response") - return - - logger.debug("Update response from keystone") - _identity_resource_rel = self.charm.model.get_relation( - relation_name, relation_id - ) - if not _identity_resource_rel: - # Relation has disappeared so skip send of data - return - - app_data = _identity_resource_rel.data[self.charm.app] - app_data["response"] = json.dumps(ops_response) diff --git a/charms/openstack-exporter-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py b/charms/openstack-exporter-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py deleted file mode 100644 index be171d8e..00000000 --- a/charms/openstack-exporter-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py +++ /dev/null @@ -1,1360 +0,0 @@ -# Copyright 2021 Canonical Ltd. -# See LICENSE file for licensing details. - - -"""Library for the tls-certificates relation. - -This library contains the Requires and Provides classes for handling the tls-certificates -interface. - -## Getting Started -From a charm directory, fetch the library using `charmcraft`: - -```shell -charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates -``` - -Add the following libraries to the charm's `requirements.txt` file: -- jsonschema -- cryptography - -Add the following section to the charm's `charmcraft.yaml` file: -```yaml -parts: - charm: - build-packages: - - libffi-dev - - libssl-dev - - rustc - - cargo -``` - -### Provider charm -The provider charm is the charm providing certificates to another charm that requires them. In -this example, the provider charm is storing its private key using a peer relation interface called -`replicas`. - -Example: -```python -from charms.tls_certificates_interface.v1.tls_certificates import ( - CertificateCreationRequestEvent, - CertificateRevocationRequestEvent, - TLSCertificatesProvidesV1, - generate_private_key, -) -from ops.charm import CharmBase, InstallEvent -from ops.main import main -from ops.model import ActiveStatus, WaitingStatus - - -def generate_ca(private_key: bytes, subject: str) -> str: - return "whatever ca content" - - -def generate_certificate(ca: str, private_key: str, csr: str) -> str: - return "Whatever certificate" - - -class ExampleProviderCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - self.certificates = TLSCertificatesProvidesV1(self, "certificates") - self.framework.observe( - self.certificates.on.certificate_request, self._on_certificate_request - ) - self.framework.observe( - self.certificates.on.certificate_revoked, self._on_certificate_revocation_request - ) - self.framework.observe(self.on.install, self._on_install) - - def _on_install(self, event: InstallEvent) -> None: - private_key_password = b"banana" - private_key = generate_private_key(password=private_key_password) - ca_certificate = generate_ca(private_key=private_key, subject="whatever") - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update( - { - "private_key_password": "banana", - "private_key": private_key, - "ca_certificate": ca_certificate, - } - ) - self.unit.status = ActiveStatus() - - def _on_certificate_request(self, event: CertificateCreationRequestEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - ca_certificate = replicas_relation.data[self.app].get("ca_certificate") - private_key = replicas_relation.data[self.app].get("private_key") - certificate = generate_certificate( - ca=ca_certificate, - private_key=private_key, - csr=event.certificate_signing_request, - ) - - self.certificates.set_relation_certificate( - certificate=certificate, - certificate_signing_request=event.certificate_signing_request, - ca=ca_certificate, - chain=[ca_certificate, certificate], - relation_id=event.relation_id, - ) - - def _on_certificate_revocation_request(self, event: CertificateRevocationRequestEvent) -> None: - # Do what you want to do with this information - pass - - -if __name__ == "__main__": - main(ExampleProviderCharm) -``` - -### Requirer charm -The requirer charm is the charm requiring certificates from another charm that provides them. In -this example, the requirer charm is storing its certificates using a peer relation interface called -`replicas`. - -Example: -```python -from charms.tls_certificates_interface.v1.tls_certificates import ( - CertificateAvailableEvent, - CertificateExpiringEvent, - CertificateRevokedEvent, - TLSCertificatesRequiresV1, - generate_csr, - generate_private_key, -) -from ops.charm import CharmBase, RelationJoinedEvent -from ops.main import main -from ops.model import ActiveStatus, WaitingStatus - - -class ExampleRequirerCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - self.cert_subject = "whatever" - self.certificates = TLSCertificatesRequiresV1(self, "certificates") - self.framework.observe(self.on.install, self._on_install) - self.framework.observe( - self.on.certificates_relation_joined, self._on_certificates_relation_joined - ) - self.framework.observe( - self.certificates.on.certificate_available, self._on_certificate_available - ) - self.framework.observe( - self.certificates.on.certificate_expiring, self._on_certificate_expiring - ) - self.framework.observe( - self.certificates.on.certificate_revoked, self._on_certificate_revoked - ) - - def _on_install(self, event) -> None: - private_key_password = b"banana" - private_key = generate_private_key(password=private_key_password) - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update( - {"private_key_password": "banana", "private_key": private_key.decode()} - ) - - def _on_certificates_relation_joined(self, event: RelationJoinedEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - replicas_relation.data[self.app].update({"csr": csr.decode()}) - self.certificates.request_certificate_creation(certificate_signing_request=csr) - - def _on_certificate_available(self, event: CertificateAvailableEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update({"certificate": event.certificate}) - replicas_relation.data[self.app].update({"ca": event.ca}) - replicas_relation.data[self.app].update({"chain": event.chain}) - self.unit.status = ActiveStatus() - - def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - old_csr = replicas_relation.data[self.app].get("csr") - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - new_csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - self.certificates.request_certificate_renewal( - old_certificate_signing_request=old_csr, - new_certificate_signing_request=new_csr, - ) - replicas_relation.data[self.app].update({"csr": new_csr.decode()}) - - def _on_certificate_revoked(self, event: CertificateRevokedEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - old_csr = replicas_relation.data[self.app].get("csr") - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - new_csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - self.certificates.request_certificate_renewal( - old_certificate_signing_request=old_csr, - new_certificate_signing_request=new_csr, - ) - replicas_relation.data[self.app].update({"csr": new_csr.decode()}) - replicas_relation.data[self.app].pop("certificate") - replicas_relation.data[self.app].pop("ca") - replicas_relation.data[self.app].pop("chain") - self.unit.status = WaitingStatus("Waiting for new certificate") - - -if __name__ == "__main__": - main(ExampleRequirerCharm) -``` -""" # noqa: D405, D410, D411, D214, D416 - -import copy -import json -import logging -import uuid -from datetime import datetime, timedelta -from ipaddress import IPv4Address -from typing import Dict, List, Optional - -from cryptography import x509 -from cryptography.hazmat._oid import ExtensionOID -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.hazmat.primitives.serialization import pkcs12 -from cryptography.x509.extensions import Extension, ExtensionNotFound -from jsonschema import exceptions, validate # type: ignore[import] -from ops.charm import CharmBase, CharmEvents, RelationChangedEvent, UpdateStatusEvent -from ops.framework import EventBase, EventSource, Handle, Object - -# The unique Charmhub library identifier, never change it -LIBID = "afd8c2bccf834997afce12c2706d2ede" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 12 - - -REQUIRER_JSON_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/requirer.json", # noqa: E501 - "type": "object", - "title": "`tls_certificates` requirer root schema", - "description": "The `tls_certificates` root schema comprises the entire requirer databag for this interface.", # noqa: E501 - "examples": [ - { - "certificate_signing_requests": [ - { - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\\n-----END CERTIFICATE REQUEST-----\\n" # noqa: E501 - }, - { - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBAMk3raaX803cHvzlBF9LC7KORT46z4VjyU5PIaMb\\nQLIDgYKFYI0n5hf2Ra4FAHvOvEmW7bjNlHORFEmvnpcU5kPMNUyKFMTaC8LGmN8z\\nUBH3aK+0+FRvY4afn9tgj5435WqOG9QdoDJ0TJkjJbJI9M70UOgL711oU7ql6HxU\\n4d2ydFK9xAHrBwziNHgNZ72L95s4gLTXf0fAHYf15mDA9U5yc+YDubCKgTXzVySQ\\nUx73VCJLfC/XkZIh559IrnRv5G9fu6BMLEuBwAz6QAO4+/XidbKWN4r2XSq5qX4n\\n6EPQQWP8/nd4myq1kbg6Q8w68L/0YdfjCmbyf2TuoWeImdUCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQBIdwraBvpYo/rl5MH1+1Um6HRg4gOdQPY5WcJy9B9tgzJz\\nittRSlRGTnhyIo6fHgq9KHrmUthNe8mMTDailKFeaqkVNVvk7l0d1/B90Kz6OfmD\\nxN0qjW53oP7y3QB5FFBM8DjqjmUnz5UePKoX4AKkDyrKWxMwGX5RoET8c/y0y9jp\\nvSq3Wh5UpaZdWbe1oVY8CqMVUEVQL2DPjtopxXFz2qACwsXkQZxWmjvZnRiP8nP8\\nbdFaEuh9Q6rZ2QdZDEtrU4AodPU3NaukFr5KlTUQt3w/cl+5//zils6G5zUWJ2pN\\ng7+t9PTvXHRkH+LnwaVnmsBFU2e05qADQbfIn7JA\\n-----END CERTIFICATE REQUEST-----\\n" # noqa: E501 - }, - ] - } - ], - "properties": { - "certificate_signing_requests": { - "type": "array", - "items": { - "type": "object", - "properties": {"certificate_signing_request": {"type": "string"}}, - "required": ["certificate_signing_request"], - }, - } - }, - "required": ["certificate_signing_requests"], - "additionalProperties": True, -} - -PROVIDER_JSON_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/provider.json", # noqa: E501 - "type": "object", - "title": "`tls_certificates` provider root schema", - "description": "The `tls_certificates` root schema comprises the entire provider databag for this interface.", # noqa: E501 - "examples": [ - { - "certificates": [ - { - "ca": "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n", # noqa: E501 - "chain": [ - "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n" # noqa: E501, W505 - ], - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\n-----END CERTIFICATE REQUEST-----\n", # noqa: E501 - "certificate": "-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n", # noqa: E501 - } - ] - }, - { - "certificates": [ - { - "ca": "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n", # noqa: E501 - "chain": [ - "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n" # noqa: E501, W505 - ], - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\n-----END CERTIFICATE REQUEST-----\n", # noqa: E501 - "certificate": "-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n", # noqa: E501 - "revoked": True, - } - ] - }, - ], - "properties": { - "certificates": { - "$id": "#/properties/certificates", - "type": "array", - "items": { - "$id": "#/properties/certificates/items", - "type": "object", - "required": ["certificate_signing_request", "certificate", "ca", "chain"], - "properties": { - "certificate_signing_request": { - "$id": "#/properties/certificates/items/certificate_signing_request", - "type": "string", - }, - "certificate": { - "$id": "#/properties/certificates/items/certificate", - "type": "string", - }, - "ca": {"$id": "#/properties/certificates/items/ca", "type": "string"}, - "chain": { - "$id": "#/properties/certificates/items/chain", - "type": "array", - "items": { - "type": "string", - "$id": "#/properties/certificates/items/chain/items", - }, - }, - "revoked": { - "$id": "#/properties/certificates/items/revoked", - "type": "boolean", - }, - }, - "additionalProperties": True, - }, - } - }, - "required": ["certificates"], - "additionalProperties": True, -} - - -logger = logging.getLogger(__name__) - - -class CertificateAvailableEvent(EventBase): - """Charm Event triggered when a TLS certificate is available.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - - -class CertificateExpiringEvent(EventBase): - """Charm Event triggered when a TLS certificate is almost expired.""" - - def __init__(self, handle, certificate: str, expiry: str): - """CertificateExpiringEvent. - - Args: - handle (Handle): Juju framework handle - certificate (str): TLS Certificate - expiry (str): Datetime string reprensenting the time at which the certificate - won't be valid anymore. - """ - super().__init__(handle) - self.certificate = certificate - self.expiry = expiry - - def snapshot(self) -> dict: - """Returns snapshot.""" - return {"certificate": self.certificate, "expiry": self.expiry} - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.expiry = snapshot["expiry"] - - -class CertificateExpiredEvent(EventBase): - """Charm Event triggered when a TLS certificate is expired.""" - - def __init__(self, handle: Handle, certificate: str): - super().__init__(handle) - self.certificate = certificate - - def snapshot(self) -> dict: - """Returns snapshot.""" - return {"certificate": self.certificate} - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - - -class CertificateRevokedEvent(EventBase): - """Charm Event triggered when a TLS certificate is revoked.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - revoked: bool, - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - self.revoked = revoked - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - "revoked": self.revoked, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - self.revoked = snapshot["revoked"] - - -class CertificateCreationRequestEvent(EventBase): - """Charm Event triggered when a TLS certificate is required.""" - - def __init__(self, handle: Handle, certificate_signing_request: str, relation_id: int): - super().__init__(handle) - self.certificate_signing_request = certificate_signing_request - self.relation_id = relation_id - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate_signing_request": self.certificate_signing_request, - "relation_id": self.relation_id, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.relation_id = snapshot["relation_id"] - - -class CertificateRevocationRequestEvent(EventBase): - """Charm Event triggered when a TLS certificate needs to be revoked.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: str, - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - - -def _load_relation_data(raw_relation_data: dict) -> dict: - """Loads relation data from the relation data bag. - - Json loads all data. - - Args: - raw_relation_data: Relation data from the databag - - Returns: - dict: Relation data in dict format. - """ - certificate_data = dict() - for key in raw_relation_data: - try: - certificate_data[key] = json.loads(raw_relation_data[key]) - except (json.decoder.JSONDecodeError, TypeError): - certificate_data[key] = raw_relation_data[key] - return certificate_data - - -def generate_ca( - private_key: bytes, - subject: str, - private_key_password: Optional[bytes] = None, - validity: int = 365, - country: str = "US", -) -> bytes: - """Generates a CA Certificate. - - Args: - private_key (bytes): Private key - subject (str): Certificate subject - private_key_password (bytes): Private key password - validity (int): Certificate validity time (in days) - country (str): Certificate Issuing country - - Returns: - bytes: CA Certificate. - """ - private_key_object = serialization.load_pem_private_key( - private_key, password=private_key_password - ) - subject = issuer = x509.Name( - [ - x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country), - x509.NameAttribute(x509.NameOID.COMMON_NAME, subject), - ] - ) - subject_identifier_object = x509.SubjectKeyIdentifier.from_public_key( - private_key_object.public_key() # type: ignore[arg-type] - ) - subject_identifier = key_identifier = subject_identifier_object.public_bytes() - cert = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(private_key_object.public_key()) # type: ignore[arg-type] - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.utcnow()) - .not_valid_after(datetime.utcnow() + timedelta(days=validity)) - .add_extension(x509.SubjectKeyIdentifier(digest=subject_identifier), critical=False) - .add_extension( - x509.AuthorityKeyIdentifier( - key_identifier=key_identifier, - authority_cert_issuer=None, - authority_cert_serial_number=None, - ), - critical=False, - ) - .add_extension( - x509.BasicConstraints(ca=True, path_length=None), - critical=True, - ) - .sign(private_key_object, hashes.SHA256()) # type: ignore[arg-type] - ) - return cert.public_bytes(serialization.Encoding.PEM) - - -def generate_certificate( - csr: bytes, - ca: bytes, - ca_key: bytes, - ca_key_password: Optional[bytes] = None, - validity: int = 365, - alt_names: Optional[List[str]] = None, -) -> bytes: - """Generates a TLS certificate based on a CSR. - - Args: - csr (bytes): CSR - ca (bytes): CA Certificate - ca_key (bytes): CA private key - ca_key_password: CA private key password - validity (int): Certificate validity (in days) - alt_names (list): List of alt names to put on cert - prefer putting SANs in CSR - - Returns: - bytes: Certificate - """ - csr_object = x509.load_pem_x509_csr(csr) - subject = csr_object.subject - issuer = x509.load_pem_x509_certificate(ca).issuer - private_key = serialization.load_pem_private_key(ca_key, password=ca_key_password) - - certificate_builder = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(csr_object.public_key()) - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.utcnow()) - .not_valid_after(datetime.utcnow() + timedelta(days=validity)) - ) - - extensions_list = csr_object.extensions - san_ext: Optional[x509.Extension] = None - if alt_names: - full_sans_dns = alt_names.copy() - try: - loaded_san_ext = csr_object.extensions.get_extension_for_class( - x509.SubjectAlternativeName - ) - full_sans_dns.extend(loaded_san_ext.value.get_values_for_type(x509.DNSName)) - except ExtensionNotFound: - pass - finally: - san_ext = Extension( - ExtensionOID.SUBJECT_ALTERNATIVE_NAME, - False, - x509.SubjectAlternativeName([x509.DNSName(name) for name in full_sans_dns]), - ) - if not extensions_list: - extensions_list = x509.Extensions([san_ext]) - - for extension in extensions_list: - if extension.value.oid == ExtensionOID.SUBJECT_ALTERNATIVE_NAME and san_ext: - extension = san_ext - - certificate_builder = certificate_builder.add_extension( - extension.value, - critical=extension.critical, - ) - certificate_builder._version = x509.Version.v3 - cert = certificate_builder.sign(private_key, hashes.SHA256()) # type: ignore[arg-type] - return cert.public_bytes(serialization.Encoding.PEM) - - -def generate_pfx_package( - certificate: bytes, - private_key: bytes, - package_password: str, - private_key_password: Optional[bytes] = None, -) -> bytes: - """Generates a PFX package to contain the TLS certificate and private key. - - Args: - certificate (bytes): TLS certificate - private_key (bytes): Private key - package_password (str): Password to open the PFX package - private_key_password (bytes): Private key password - - Returns: - bytes: - """ - private_key_object = serialization.load_pem_private_key( - private_key, password=private_key_password - ) - certificate_object = x509.load_pem_x509_certificate(certificate) - name = certificate_object.subject.rfc4514_string() - pfx_bytes = pkcs12.serialize_key_and_certificates( - name=name.encode(), - cert=certificate_object, - key=private_key_object, # type: ignore[arg-type] - cas=None, - encryption_algorithm=serialization.BestAvailableEncryption(package_password.encode()), - ) - return pfx_bytes - - -def generate_private_key( - password: Optional[bytes] = None, - key_size: int = 2048, - public_exponent: int = 65537, -) -> bytes: - """Generates a private key. - - Args: - password (bytes): Password for decrypting the private key - key_size (int): Key size in bytes - public_exponent: Public exponent. - - Returns: - bytes: Private Key - """ - private_key = rsa.generate_private_key( - public_exponent=public_exponent, - key_size=key_size, - ) - key_bytes = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.BestAvailableEncryption(password) - if password - else serialization.NoEncryption(), - ) - return key_bytes - - -def generate_csr( - private_key: bytes, - subject: str, - add_unique_id_to_subject_name: bool = True, - organization: Optional[str] = None, - email_address: Optional[str] = None, - country_name: Optional[str] = None, - private_key_password: Optional[bytes] = None, - sans: Optional[List[str]] = None, - sans_oid: Optional[List[str]] = None, - sans_ip: Optional[List[str]] = None, - sans_dns: Optional[List[str]] = None, - additional_critical_extensions: Optional[List] = None, -) -> bytes: - """Generates a CSR using private key and subject. - - Args: - private_key (bytes): Private key - subject (str): CSR Subject. - add_unique_id_to_subject_name (bool): Whether a unique ID must be added to the CSR's - subject name. Always leave to "True" when the CSR is used to request certificates - using the tls-certificates relation. - organization (str): Name of organization. - email_address (str): Email address. - country_name (str): Country Name. - private_key_password (bytes): Private key password - sans (list): Use sans_dns - this will be deprecated in a future release - List of DNS subject alternative names (keeping it for now for backward compatibility) - sans_oid (list): List of registered ID SANs - sans_dns (list): List of DNS subject alternative names (similar to the arg: sans) - sans_ip (list): List of IP subject alternative names - additional_critical_extensions (list): List if critical additional extension objects. - Object must be a x509 ExtensionType. - - Returns: - bytes: CSR - """ - signing_key = serialization.load_pem_private_key(private_key, password=private_key_password) - subject_name = [x509.NameAttribute(x509.NameOID.COMMON_NAME, subject)] - if add_unique_id_to_subject_name: - unique_identifier = uuid.uuid4() - subject_name.append( - x509.NameAttribute(x509.NameOID.X500_UNIQUE_IDENTIFIER, str(unique_identifier)) - ) - if organization: - subject_name.append(x509.NameAttribute(x509.NameOID.ORGANIZATION_NAME, organization)) - if email_address: - subject_name.append(x509.NameAttribute(x509.NameOID.EMAIL_ADDRESS, email_address)) - if country_name: - subject_name.append(x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country_name)) - csr = x509.CertificateSigningRequestBuilder(subject_name=x509.Name(subject_name)) - - _sans: List[x509.GeneralName] = [] - if sans_oid: - _sans.extend([x509.RegisteredID(x509.ObjectIdentifier(san)) for san in sans_oid]) - if sans_ip: - _sans.extend([x509.IPAddress(IPv4Address(san)) for san in sans_ip]) - if sans: - _sans.extend([x509.DNSName(san) for san in sans]) - if sans_dns: - _sans.extend([x509.DNSName(san) for san in sans_dns]) - if _sans: - csr = csr.add_extension(x509.SubjectAlternativeName(set(_sans)), critical=False) - - if additional_critical_extensions: - for extension in additional_critical_extensions: - csr = csr.add_extension(extension, critical=True) - - signed_certificate = csr.sign(signing_key, hashes.SHA256()) # type: ignore[arg-type] - return signed_certificate.public_bytes(serialization.Encoding.PEM) - - -class CertificatesProviderCharmEvents(CharmEvents): - """List of events that the TLS Certificates provider charm can leverage.""" - - certificate_creation_request = EventSource(CertificateCreationRequestEvent) - certificate_revocation_request = EventSource(CertificateRevocationRequestEvent) - - -class CertificatesRequirerCharmEvents(CharmEvents): - """List of events that the TLS Certificates requirer charm can leverage.""" - - certificate_available = EventSource(CertificateAvailableEvent) - certificate_expiring = EventSource(CertificateExpiringEvent) - certificate_expired = EventSource(CertificateExpiredEvent) - certificate_revoked = EventSource(CertificateRevokedEvent) - - -class TLSCertificatesProvidesV1(Object): - """TLS certificates provider class to be instantiated by TLS certificates providers.""" - - on = CertificatesProviderCharmEvents() - - def __init__(self, charm: CharmBase, relationship_name: str): - super().__init__(charm, relationship_name) - self.framework.observe( - charm.on[relationship_name].relation_changed, self._on_relation_changed - ) - self.charm = charm - self.relationship_name = relationship_name - - def _add_certificate( - self, - relation_id: int, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - ) -> None: - """Adds certificate to relation data. - - Args: - relation_id (int): Relation id - certificate (str): Certificate - certificate_signing_request (str): Certificate Signing Request - ca (str): CA Certificate - chain (list): CA Chain - - Returns: - None - """ - relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - new_certificate = { - "certificate": certificate, - "certificate_signing_request": certificate_signing_request, - "ca": ca, - "chain": chain, - } - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = provider_relation_data.get("certificates", []) - certificates = copy.deepcopy(provider_certificates) - if new_certificate in certificates: - logger.info("Certificate already in relation data - Doing nothing") - return - certificates.append(new_certificate) - relation.data[self.model.app]["certificates"] = json.dumps(certificates) - - def _remove_certificate( - self, - relation_id: int, - certificate: Optional[str] = None, - certificate_signing_request: Optional[str] = None, - ) -> None: - """Removes certificate from a given relation based on user provided certificate or csr. - - Args: - relation_id (int): Relation id - certificate (str): Certificate (optional) - certificate_signing_request: Certificate signing request (optional) - - Returns: - None - """ - relation = self.model.get_relation( - relation_name=self.relationship_name, - relation_id=relation_id, - ) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} with relation id {relation_id} does not exist" - ) - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = provider_relation_data.get("certificates", []) - certificates = copy.deepcopy(provider_certificates) - for certificate_dict in certificates: - if certificate and certificate_dict["certificate"] == certificate: - certificates.remove(certificate_dict) - if ( - certificate_signing_request - and certificate_dict["certificate_signing_request"] == certificate_signing_request - ): - certificates.remove(certificate_dict) - relation.data[self.model.app]["certificates"] = json.dumps(certificates) - - @staticmethod - def _relation_data_is_valid(certificates_data: dict) -> bool: - """Uses JSON schema validator to validate relation data content. - - Args: - certificates_data (dict): Certificate data dictionary as retrieved from relation data. - - Returns: - bool: True/False depending on whether the relation data follows the json schema. - """ - try: - validate(instance=certificates_data, schema=REQUIRER_JSON_SCHEMA) - return True - except exceptions.ValidationError: - return False - - def revoke_all_certificates(self) -> None: - """Revokes all certificates of this provider. - - This method is meant to be used when the Root CA has changed. - """ - for relation in self.model.relations[self.relationship_name]: - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = copy.deepcopy(provider_relation_data.get("certificates", [])) - for certificate in provider_certificates: - certificate["revoked"] = True - relation.data[self.model.app]["certificates"] = json.dumps(provider_certificates) - - def set_relation_certificate( - self, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - relation_id: int, - ) -> None: - """Adds certificates to relation data. - - Args: - certificate (str): Certificate - certificate_signing_request (str): Certificate signing request - ca (str): CA Certificate - chain (list): CA Chain - relation_id (int): Juju relation ID - - Returns: - None - """ - certificates_relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - self._remove_certificate( - certificate_signing_request=certificate_signing_request.strip(), - relation_id=relation_id, - ) - self._add_certificate( - relation_id=relation_id, - certificate=certificate.strip(), - certificate_signing_request=certificate_signing_request.strip(), - ca=ca.strip(), - chain=[cert.strip() for cert in chain], - ) - - def remove_certificate(self, certificate: str) -> None: - """Removes a given certificate from relation data. - - Args: - certificate (str): TLS Certificate - - Returns: - None - """ - certificates_relation = self.model.relations[self.relationship_name] - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - for certificate_relation in certificates_relation: - self._remove_certificate(certificate=certificate, relation_id=certificate_relation.id) - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handler triggerred on relation changed event. - - Looks at the relation data and either emits: - - certificate request event: If the unit relation data contains a CSR for which - a certificate does not exist in the provider relation data. - - certificate revocation event: If the provider relation data contains a CSR for which - a csr does not exist in the requirer relation data. - - Args: - event: Juju event - - Returns: - None - """ - assert event.unit is not None - requirer_relation_data = _load_relation_data(event.relation.data[event.unit]) - provider_relation_data = _load_relation_data(event.relation.data[self.charm.app]) - if not self._relation_data_is_valid(requirer_relation_data): - logger.warning( - f"Relation data did not pass JSON Schema validation: {requirer_relation_data}" - ) - return - provider_certificates = provider_relation_data.get("certificates", []) - requirer_csrs = requirer_relation_data.get("certificate_signing_requests", []) - provider_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in provider_certificates - ] - requirer_unit_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in requirer_csrs - ] - for certificate_signing_request in requirer_unit_csrs: - if certificate_signing_request not in provider_csrs: - self.on.certificate_creation_request.emit( - certificate_signing_request=certificate_signing_request, - relation_id=event.relation.id, - ) - self._revoke_certificates_for_which_no_csr_exists(relation_id=event.relation.id) - - def _revoke_certificates_for_which_no_csr_exists(self, relation_id: int) -> None: - """Revokes certificates for which no unit has a CSR. - - Goes through all generated certificates and compare agains the list of CSRS for all units - of a given relationship. - - Args: - relation_id (int): Relation id - - Returns: - None - """ - certificates_relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - provider_relation_data = _load_relation_data(certificates_relation.data[self.charm.app]) - list_of_csrs: List[str] = [] - for unit in certificates_relation.units: - requirer_relation_data = _load_relation_data(certificates_relation.data[unit]) - requirer_csrs = requirer_relation_data.get("certificate_signing_requests", []) - list_of_csrs.extend(csr["certificate_signing_request"] for csr in requirer_csrs) - provider_certificates = provider_relation_data.get("certificates", []) - for certificate in provider_certificates: - if certificate["certificate_signing_request"] not in list_of_csrs: - self.on.certificate_revocation_request.emit( - certificate=certificate["certificate"], - certificate_signing_request=certificate["certificate_signing_request"], - ca=certificate["ca"], - chain=certificate["chain"], - ) - self.remove_certificate(certificate=certificate["certificate"]) - - -class TLSCertificatesRequiresV1(Object): - """TLS certificates requirer class to be instantiated by TLS certificates requirers.""" - - on = CertificatesRequirerCharmEvents() - - def __init__( - self, - charm: CharmBase, - relationship_name: str, - expiry_notification_time: int = 168, - ): - """Generates/use private key and observes relation changed event. - - Args: - charm: Charm object - relationship_name: Juju relation name - expiry_notification_time (int): Time difference between now and expiry (in hours). - Used to trigger the CertificateExpiring event. Default: 7 days. - """ - super().__init__(charm, relationship_name) - self.relationship_name = relationship_name - self.charm = charm - self.expiry_notification_time = expiry_notification_time - self.framework.observe( - charm.on[relationship_name].relation_changed, self._on_relation_changed - ) - self.framework.observe(charm.on.update_status, self._on_update_status) - - @property - def _requirer_csrs(self) -> List[Dict[str, str]]: - """Returns list of requirer CSR's from relation data.""" - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - requirer_relation_data = _load_relation_data(relation.data[self.model.unit]) - return requirer_relation_data.get("certificate_signing_requests", []) - - @property - def _provider_certificates(self) -> List[Dict[str, str]]: - """Returns list of provider CSR's from relation data.""" - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - if not relation.app: - raise RuntimeError(f"Remote app for relation {self.relationship_name} does not exist") - provider_relation_data = _load_relation_data(relation.data[relation.app]) - return provider_relation_data.get("certificates", []) - - def _add_requirer_csr(self, csr: str) -> None: - """Adds CSR to relation data. - - Args: - csr (str): Certificate Signing Request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - new_csr_dict = {"certificate_signing_request": csr} - if new_csr_dict in self._requirer_csrs: - logger.info("CSR already in relation data - Doing nothing") - return - requirer_csrs = copy.deepcopy(self._requirer_csrs) - requirer_csrs.append(new_csr_dict) - relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs) - - def _remove_requirer_csr(self, csr: str) -> None: - """Removes CSR from relation data. - - Args: - csr (str): Certificate signing request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - requirer_csrs = copy.deepcopy(self._requirer_csrs) - csr_dict = {"certificate_signing_request": csr} - if csr_dict not in requirer_csrs: - logger.info("CSR not in relation data - Doing nothing") - return - requirer_csrs.remove(csr_dict) - relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs) - - def request_certificate_creation(self, certificate_signing_request: bytes) -> None: - """Request TLS certificate to provider charm. - - Args: - certificate_signing_request (bytes): Certificate Signing Request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - message = ( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - logger.error(message) - raise RuntimeError(message) - self._add_requirer_csr(certificate_signing_request.decode().strip()) - logger.info("Certificate request sent to provider") - - def request_certificate_revocation(self, certificate_signing_request: bytes) -> None: - """Removes CSR from relation data. - - The provider of this relation is then expected to remove certificates associated to this - CSR from the relation data as well and emit a request_certificate_revocation event for the - provider charm to interpret. - - Args: - certificate_signing_request (bytes): Certificate Signing Request - - Returns: - None - """ - self._remove_requirer_csr(certificate_signing_request.decode().strip()) - logger.info("Certificate revocation sent to provider") - - def request_certificate_renewal( - self, old_certificate_signing_request: bytes, new_certificate_signing_request: bytes - ) -> None: - """Renews certificate. - - Removes old CSR from relation data and adds new one. - - Args: - old_certificate_signing_request: Old CSR - new_certificate_signing_request: New CSR - - Returns: - None - """ - try: - self.request_certificate_revocation( - certificate_signing_request=old_certificate_signing_request - ) - except RuntimeError: - logger.warning("Certificate revocation failed.") - self.request_certificate_creation( - certificate_signing_request=new_certificate_signing_request - ) - logger.info("Certificate renewal request completed.") - - @staticmethod - def _relation_data_is_valid(certificates_data: dict) -> bool: - """Checks whether relation data is valid based on json schema. - - Args: - certificates_data: Certificate data in dict format. - - Returns: - bool: Whether relation data is valid. - """ - try: - validate(instance=certificates_data, schema=PROVIDER_JSON_SCHEMA) - return True - except exceptions.ValidationError: - return False - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handler triggered on relation changed events. - - Args: - event: Juju event - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - logger.warning(f"No relation: {self.relationship_name}") - return - if not relation.app: - logger.warning(f"No remote app in relation: {self.relationship_name}") - return - provider_relation_data = _load_relation_data(relation.data[relation.app]) - if not self._relation_data_is_valid(provider_relation_data): - logger.warning( - f"Provider relation data did not pass JSON Schema validation: " - f"{event.relation.data[relation.app]}" - ) - return - requirer_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in self._requirer_csrs - ] - for certificate in self._provider_certificates: - if certificate["certificate_signing_request"] in requirer_csrs: - if certificate.get("revoked", False): - self.on.certificate_revoked.emit( - certificate_signing_request=certificate["certificate_signing_request"], - certificate=certificate["certificate"], - ca=certificate["ca"], - chain=certificate["chain"], - revoked=True, - ) - else: - self.on.certificate_available.emit( - certificate_signing_request=certificate["certificate_signing_request"], - certificate=certificate["certificate"], - ca=certificate["ca"], - chain=certificate["chain"], - ) - - def _on_update_status(self, event: UpdateStatusEvent) -> None: - """Triggered on update status event. - - Goes through each certificate in the "certificates" relation and checks their expiry date. - If they are close to expire (<7 days), emits a CertificateExpiringEvent event and if - they are expired, emits a CertificateExpiredEvent. - - Args: - event (UpdateStatusEvent): Juju event - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - logger.warning(f"No relation: {self.relationship_name}") - return - if not relation.app: - logger.warning(f"No remote app in relation: {self.relationship_name}") - return - provider_relation_data = _load_relation_data(relation.data[relation.app]) - if not self._relation_data_is_valid(provider_relation_data): - logger.warning( - f"Provider relation data did not pass JSON Schema validation: " - f"{relation.data[relation.app]}" - ) - return - for certificate_dict in self._provider_certificates: - certificate = certificate_dict["certificate"] - try: - certificate_object = x509.load_pem_x509_certificate(data=certificate.encode()) - except ValueError: - logger.warning("Could not load certificate.") - continue - time_difference = certificate_object.not_valid_after - datetime.utcnow() - if time_difference.total_seconds() < 0: - logger.warning("Certificate is expired") - self.on.certificate_expired.emit(certificate=certificate) - self.request_certificate_revocation(certificate.encode()) - continue - if time_difference.total_seconds() < (self.expiry_notification_time * 60 * 60): - logger.warning("Certificate almost expired") - self.on.certificate_expiring.emit( - certificate=certificate, expiry=certificate_object.not_valid_after.isoformat() - ) diff --git a/charms/openstack-exporter-k8s/osci.yaml b/charms/openstack-exporter-k8s/osci.yaml deleted file mode 100644 index b56ba0eb..00000000 --- a/charms/openstack-exporter-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: openstack-exporter-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/openstack-exporter-k8s/pyproject.toml b/charms/openstack-exporter-k8s/pyproject.toml deleted file mode 100644 index 30821404..00000000 --- a/charms/openstack-exporter-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/openstack-exporter-k8s/rename.sh b/charms/openstack-exporter-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/openstack-exporter-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/openstack-exporter-k8s/requirements.txt b/charms/openstack-exporter-k8s/requirements.txt index 3b37feec..48fb8600 100644 --- a/charms/openstack-exporter-k8s/requirements.txt +++ b/charms/openstack-exporter-k8s/requirements.txt @@ -1,5 +1,9 @@ ops jinja2 -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam +lightkube +lightkube-models # COS requirement cosl + +# From ops_sunbeam +tenacity diff --git a/charms/openstack-exporter-k8s/test-requirements.txt b/charms/openstack-exporter-k8s/test-requirements.txt deleted file mode 100644 index d1a61d34..00000000 --- a/charms/openstack-exporter-k8s/test-requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -# This file is managed centrally. If you find the need to modify this as a -# one-off, please don't. Intead, consult #openstack-charms and ask about -# requirements management in charms via bot-control. Thank you. - -coverage -mock -flake8 -stestr -ops diff --git a/charms/openstack-exporter-k8s/tests/unit/test_os_exporter.py b/charms/openstack-exporter-k8s/tests/unit/test_os_exporter.py index ecc89af4..88c4f24e 100644 --- a/charms/openstack-exporter-k8s/tests/unit/test_os_exporter.py +++ b/charms/openstack-exporter-k8s/tests/unit/test_os_exporter.py @@ -18,6 +18,7 @@ import json +import charm import ops_sunbeam.test_utils as test_utils from mock import ( Mock, @@ -26,8 +27,6 @@ from ops.testing import ( Harness, ) -import charm - class _OSExporterTestOperatorCharm(charm.OSExporterOperatorCharm): """Test Operator Charm for Openstack Exporter Operator.""" diff --git a/charms/openstack-exporter-k8s/tox.ini b/charms/openstack-exporter-k8s/tox.ini deleted file mode 100644 index 0bc536c1..00000000 --- a/charms/openstack-exporter-k8s/tox.ini +++ /dev/null @@ -1,161 +0,0 @@ -# Operator charm (with zaza): tox.ini - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - HOME - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/openstack-hypervisor/.gitignore b/charms/openstack-hypervisor/.gitignore deleted file mode 100644 index 33d25ac9..00000000 --- a/charms/openstack-hypervisor/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -venv/ -build/ -*.charm -.tox/ -.coverage -__pycache__/ -*.py[cod] -.idea -.vscode/ -.stestr/ diff --git a/charms/openstack-hypervisor/.gitreview b/charms/openstack-hypervisor/.gitreview deleted file mode 100644 index afdea8f1..00000000 --- a/charms/openstack-hypervisor/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-openstack-hypervisor.git -defaultbranch=main diff --git a/charms/openstack-hypervisor/.stestr.conf b/charms/openstack-hypervisor/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/openstack-hypervisor/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/openstack-hypervisor/.zuul.yaml b/charms/openstack-hypervisor/.zuul.yaml deleted file mode 100644 index fd8a2c1d..00000000 --- a/charms/openstack-hypervisor/.zuul.yaml +++ /dev/null @@ -1,12 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - prometheus-alert-rules-test - vars: - charm_build_name: openstack-hypervisor - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false - prometheus_alerts_test_rules_dir: tests/unit/test_alert_rules diff --git a/charms/openstack-hypervisor/charmcraft.yaml b/charms/openstack-hypervisor/charmcraft.yaml index c82f3030..9a8d8b29 100644 --- a/charms/openstack-hypervisor/charmcraft.yaml +++ b/charms/openstack-hypervisor/charmcraft.yaml @@ -23,4 +23,3 @@ parts: - cryptography - jsonschema - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/openstack-hypervisor/lib/charms/ceilometer_k8s/v0/ceilometer_service.py b/charms/openstack-hypervisor/lib/charms/ceilometer_k8s/v0/ceilometer_service.py deleted file mode 100644 index 016e1ba2..00000000 --- a/charms/openstack-hypervisor/lib/charms/ceilometer_k8s/v0/ceilometer_service.py +++ /dev/null @@ -1,224 +0,0 @@ -"""CeilometerServiceProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the ceilometer_service interface. - -Import `CeilometerServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "ceilometer_service" - -Two events are also available to respond to: - - config_changed - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.ceilometer_k8s.v0.ceilometer_service import ( - CeilometerServiceRequires -) - -class CeilometerServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # CeilometerService Requires - self.ceilometer_service = CeilometerServiceRequires( - self, "ceilometer_service", - ) - self.framework.observe( - self.ceilometer_service.on.config_changed, - self._on_ceilometer_service_config_changed - ) - self.framework.observe( - self.ceilometer_service.on.goneaway, - self._on_ceiometer_service_goneaway - ) - - def _on_ceilometer_service_config_changed(self, event): - '''React to the Ceilometer service config changed event. - - This event happens when CeilometerService relation is added to the - model and relation data is changed. - ''' - # Do something with the configuration provided by relation. - pass - - def _on_ceilometer_service_goneaway(self, event): - '''React to the CeilometerService goneaway event. - - This event happens when CeilometerService relation is removed. - ''' - # CeilometerService Relation has goneaway. - pass -``` -""" - -import logging -from typing import ( - Optional, -) - -from ops.charm import ( - CharmBase, - RelationBrokenEvent, - RelationChangedEvent, - RelationEvent, -) -from ops.framework import ( - EventSource, - Object, - ObjectEvents, -) -from ops.model import ( - Relation, -) - -logger = logging.getLogger(__name__) - - -# The unique Charmhub library identifier, never change it -LIBID = "fcbb94e7a18740729eaf9e2c3b90017f" - -# 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 - - -class CeilometerConfigRequestEvent(RelationEvent): - """CeilometerConfigRequest Event.""" - - pass - - -class CeilometerServiceProviderEvents(ObjectEvents): - """Events class for `on`.""" - - config_request = EventSource(CeilometerConfigRequestEvent) - - -class CeilometerServiceProvides(Object): - """CeilometerServiceProvides class.""" - - on = CeilometerServiceProviderEvents() - - def __init__(self, charm: CharmBase, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_ceilometer_service_relation_changed, - ) - - def _on_ceilometer_service_relation_changed( - self, event: RelationChangedEvent - ): - """Handle CeilometerService relation changed.""" - logging.debug("CeilometerService relation changed") - self.on.config_request.emit(event.relation) - - def set_config( - self, relation: Optional[Relation], telemetry_secret: str - ) -> None: - """Set ceilometer configuration on the relation.""" - if not self.charm.unit.is_leader(): - logging.debug("Not a leader unit, skipping set config") - return - - # If relation is not provided send config to all the related - # applications. This happens usually when config data is - # updated by provider and wants to send the data to all - # related applications - if relation is None: - logging.debug( - "Sending config to all related applications of relation" - f"{self.relation_name}" - ) - for relation in self.framework.model.relations[self.relation_name]: - relation.data[self.charm.app][ - "telemetry-secret" - ] = telemetry_secret - else: - logging.debug( - f"Sending config on relation {relation.app.name} " - f"{relation.name}/{relation.id}" - ) - relation.data[self.charm.app][ - "telemetry-secret" - ] = telemetry_secret - - -class CeilometerConfigChangedEvent(RelationEvent): - """CeilometerConfigChanged Event.""" - - pass - - -class CeilometerServiceGoneAwayEvent(RelationEvent): - """CeilometerServiceGoneAway Event.""" - - pass - - -class CeilometerServiceRequirerEvents(ObjectEvents): - """Events class for `on`.""" - - config_changed = EventSource(CeilometerConfigChangedEvent) - goneaway = EventSource(CeilometerServiceGoneAwayEvent) - - -class CeilometerServiceRequires(Object): - """CeilometerServiceRequires class.""" - - on = CeilometerServiceRequirerEvents() - - def __init__(self, charm: CharmBase, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_ceilometer_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_ceilometer_service_relation_broken, - ) - - def _on_ceilometer_service_relation_changed( - self, event: RelationChangedEvent - ): - """Handle CeilometerService relation changed.""" - logging.debug("CeilometerService config data changed") - self.on.config_changed.emit(event.relation) - - def _on_ceilometer_service_relation_broken( - self, event: RelationBrokenEvent - ): - """Handle CeilometerService relation changed.""" - logging.debug("CeilometerService on_broken") - self.on.goneaway.emit(event.relation) - - @property - def _ceilometer_service_rel(self) -> Optional[Relation]: - """The ceilometer service relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> Optional[str]: - """Return the value for the given key from remote app data.""" - if self._ceilometer_service_rel: - data = self._ceilometer_service_rel.data[ - self._ceilometer_service_rel.app - ] - return data.get(key) - - return None - - @property - def telemetry_secret(self) -> Optional[str]: - """Return the telemetry_secret.""" - return self.get_remote_app_data("telemetry-secret") diff --git a/charms/openstack-hypervisor/lib/charms/cinder_ceph_k8s/v0/ceph_access.py b/charms/openstack-hypervisor/lib/charms/cinder_ceph_k8s/v0/ceph_access.py deleted file mode 100644 index 94c5fe6d..00000000 --- a/charms/openstack-hypervisor/lib/charms/cinder_ceph_k8s/v0/ceph_access.py +++ /dev/null @@ -1,266 +0,0 @@ -"""CephAccess Provides and Requires module. - -This library contains the Requires and Provides classes for handling -the ceph-access interface. - -Import `CephAccessRequires` in your charm, with the charm object and the -relation name: - - self - - "ceph_access" - -Three events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.cinder_ceph_k8s.v0.ceph_access import CephAccessRequires - -class CephAccessClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # CephAccess Requires - self.ceph_access = CephAccessRequires( - self, - relation_name="ceph_access", - ) - self.framework.observe( - self.ceph_access.on.connected, self._on_ceph_access_connected) - self.framework.observe( - self.ceph_access.on.ready, self._on_ceph_access_ready) - self.framework.observe( - self.ceph_access.on.goneaway, self._on_ceph_access_goneaway) - - def _on_ceph_access_connected(self, event): - '''React to the CephAccess connected event. - - This event happens when n CephAccess relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_ceph_access_ready(self, event): - '''React to the CephAccess ready event. - - This event happens when an CephAccess relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass - -``` - -""" - -# The unique Charmhub library identifier, never change it -LIBID = "7fa8d4f8407c4f31ab1deb51c0c046f1" - -# 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 - -import logging -from ops.model import ( - Relation, - SecretNotFoundError, -) -from ops.framework import ( - EventBase, - ObjectEvents, - EventSource, - Object, -) -logger = logging.getLogger(__name__) - -class CephAccessConnectedEvent(EventBase): - """CephAccess connected Event.""" - - pass - - -class CephAccessReadyEvent(EventBase): - """CephAccess ready for use Event.""" - - pass - - -class CephAccessGoneAwayEvent(EventBase): - """CephAccess relation has gone-away Event""" - - pass - - -class CephAccessServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(CephAccessConnectedEvent) - ready = EventSource(CephAccessReadyEvent) - goneaway = EventSource(CephAccessGoneAwayEvent) - - -class CephAccessRequires(Object): - """ - CephAccessRequires class - """ - - - on = CephAccessServerEvents() - - def __init__(self, charm, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_ceph_access_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_ceph_access_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_ceph_access_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_ceph_access_relation_broken, - ) - - @property - def _ceph_access_rel(self) -> Relation: - """The CephAccess relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._ceph_access_rel.data[self._ceph_access_rel.app] - return data.get(key) - - def _on_ceph_access_relation_joined(self, event): - """CephAccess relation joined.""" - logging.debug("CephAccess on_joined") - self.on.connected.emit() - - def _on_ceph_access_relation_changed(self, event): - """CephAccess relation changed.""" - logging.debug("CephAccess on_changed") - try: - if self.ready: - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_ceph_access_relation_broken(self, event): - """CephAccess relation broken.""" - logging.debug("CephAccess on_broken") - self.on.goneaway.emit() - - def _retrieve_secret(self): - try: - credentials_id = self.get_remote_app_data('access-credentials') - if not credentials_id: - return None - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def ceph_access_data(self): - """Return the service_password.""" - secret = self._retrieve_secret() - if not secret: - return {} - return secret.get_content() - - @property - def ready(self) -> str: - """Return the service_password.""" - return all(k in self.ceph_access_data for k in ["uuid", "key"]) - -class HasCephAccessClientsEvent(EventBase): - """Has CephAccessClients Event.""" - - pass - -class ReadyCephAccessClientsEvent(EventBase): - """Has CephAccessClients Event.""" - - def __init__(self, handle, relation_id): - super().__init__(handle) - self.relation_id = relation_id - - def snapshot(self): - return {"relation_id": self.relation_id} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - -class CephAccessClientEvents(ObjectEvents): - """Events class for `on`""" - - has_ceph_access_clients = EventSource(HasCephAccessClientsEvent) - ready_ceph_access_clients = EventSource(ReadyCephAccessClientsEvent) - - -class CephAccessProvides(Object): - """ - CephAccessProvides class - """ - - on = CephAccessClientEvents() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_ceph_access_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_ceph_access_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_ceph_access_relation_broken, - ) - - def _on_ceph_access_relation_joined(self, event): - """Handle CephAccess joined.""" - logging.debug("CephAccess on_joined") - self.on.has_ceph_access_clients.emit() - - def _on_ceph_access_relation_changed(self, event): - """Handle CephAccess joined.""" - logging.debug("CephAccess on_changed") - self.on.ready_ceph_access_clients.emit(event.relation.id) - - def _on_ceph_access_relation_broken(self, event): - """Handle CephAccess broken.""" - logging.debug("CephAccessProvides on_broken") - - def set_ceph_access_credentials(self, relation_name: int, - relation_id: str, - access_credentials: str): - - logging.debug("Setting ceph_access connection information.") - _ceph_access_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _ceph_access_rel = relation - if not _ceph_access_rel: - # Relation has disappeared so skip send of data - return - app_data = _ceph_access_rel.data[self.charm.app] - logging.debug(access_credentials) - app_data["access-credentials"] = access_credentials diff --git a/charms/openstack-hypervisor/lib/charms/data_platform_libs/v0/database_requires.py b/charms/openstack-hypervisor/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 53d61912..00000000 --- a/charms/openstack-hypervisor/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,496 +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. - -"""Relation 'requires' side abstraction for database relation. - -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.database_requires import 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.database_requires import 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, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(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 password(self) -> Optional[str]: - """Returns the password for the created user.""" - return self.relation.data[self.relation.app].get("password") - - @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 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") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - return self.relation.data[self.relation.app].get("username") - - @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(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(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) - - -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 — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() - - 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) - self.charm = charm - self.database = database_name - 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.relations_aliases = relations_aliases - 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 - ) - - # 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 _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. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].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] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - 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 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) - - 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") - - @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]) diff --git a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/cloud_credentials.py b/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/cloud_credentials.py deleted file mode 100644 index 6253f738..00000000 --- a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/cloud_credentials.py +++ /dev/null @@ -1,418 +0,0 @@ -"""CloudCredentialsProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the cloud_credentials interface. - -Import `CloudCredentialsRequires` in your charm, with the charm object and the -relation name: - - self - - "cloud_credentials" - -Also provide additional parameters to the charm object: - - service - - internal_url - - public_url - - admin_url - - region - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v0.cloud_credentials import CloudCredentialsRequires - -class CloudCredentialsClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # CloudCredentials Requires - self.cloud_credentials = CloudCredentialsRequires( - self, "cloud_credentials", - service = "my-service" - internal_url = "http://internal-url" - public_url = "http://public-url" - admin_url = "http://admin-url" - region = "region" - ) - self.framework.observe( - self.cloud_credentials.on.connected, self._on_cloud_credentials_connected) - self.framework.observe( - self.cloud_credentials.on.ready, self._on_cloud_credentials_ready) - self.framework.observe( - self.cloud_credentials.on.goneaway, self._on_cloud_credentials_goneaway) - - def _on_cloud_credentials_connected(self, event): - '''React to the CloudCredentials connected event. - - This event happens when n CloudCredentials relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_cloud_credentials_ready(self, event): - '''React to the CloudCredentials ready event. - - The CloudCredentials interface will use the provided config for the - request to the identity server. - ''' - # CloudCredentials Relation is ready. Do something with the completed relation. - pass - - def _on_cloud_credentials_goneaway(self, event): - '''React to the CloudCredentials goneaway event. - - This event happens when an CloudCredentials relation is removed. - ''' - # CloudCredentials Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "a5d96cc2686c47eea554ce2210c2d24e" - -# 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 = 2 - -logger = logging.getLogger(__name__) - - -class CloudCredentialsConnectedEvent(EventBase): - """CloudCredentials connected Event.""" - - pass - - -class CloudCredentialsReadyEvent(EventBase): - """CloudCredentials ready for use Event.""" - - pass - - -class CloudCredentialsGoneAwayEvent(EventBase): - """CloudCredentials relation has gone-away Event""" - - pass - - -class CloudCredentialsServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(CloudCredentialsConnectedEvent) - ready = EventSource(CloudCredentialsReadyEvent) - goneaway = EventSource(CloudCredentialsGoneAwayEvent) - - -class CloudCredentialsRequires(Object): - """ - CloudCredentialsRequires class - """ - - on = CloudCredentialsServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_cloud_credentials_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_cloud_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_cloud_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_cloud_credentials_relation_broken, - ) - - def _on_cloud_credentials_relation_joined(self, event): - """CloudCredentials relation joined.""" - logging.debug("CloudCredentials on_joined") - self.on.connected.emit() - self.request_credentials() - - def _on_cloud_credentials_relation_changed(self, event): - """CloudCredentials relation changed.""" - logging.debug("CloudCredentials on_changed") - try: - self.on.ready.emit() - except (AttributeError, KeyError): - logger.exception('Error when emitting event') - - def _on_cloud_credentials_relation_broken(self, event): - """CloudCredentials relation broken.""" - logging.debug("CloudCredentials on_broken") - self.on.goneaway.emit() - - @property - def _cloud_credentials_rel(self) -> Relation: - """The CloudCredentials relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._cloud_credentials_rel.data[self._cloud_credentials_rel.app] - return data.get(key) - - @property - def api_version(self) -> str: - """Return the api_version.""" - return self.get_remote_app_data('api-version') - - @property - def auth_host(self) -> str: - """Return the auth_host.""" - return self.get_remote_app_data('auth-host') - - @property - def auth_port(self) -> str: - """Return the auth_port.""" - return self.get_remote_app_data('auth-port') - - @property - def auth_protocol(self) -> str: - """Return the auth_protocol.""" - return self.get_remote_app_data('auth-protocol') - - @property - def internal_host(self) -> str: - """Return the internal_host.""" - return self.get_remote_app_data('internal-host') - - @property - def internal_port(self) -> str: - """Return the internal_port.""" - return self.get_remote_app_data('internal-port') - - @property - def internal_protocol(self) -> str: - """Return the internal_protocol.""" - return self.get_remote_app_data('internal-protocol') - - @property - def username(self) -> str: - """Return the username.""" - return self.get_remote_app_data('username') - - @property - def password(self) -> str: - """Return the password.""" - return self.get_remote_app_data('password') - - @property - def project_name(self) -> str: - """Return the project name.""" - return self.get_remote_app_data('project-name') - - @property - def project_id(self) -> str: - """Return the project id.""" - return self.get_remote_app_data('project-id') - - @property - def user_domain_name(self) -> str: - """Return the name of the user domain.""" - return self.get_remote_app_data('user-domain-name') - - @property - def user_domain_id(self) -> str: - """Return the id of the user domain.""" - return self.get_remote_app_data('user-domain-id') - - @property - def project_domain_name(self) -> str: - """Return the name of the project domain.""" - return self.get_remote_app_data('project-domain-name') - - @property - def project_domain_id(self) -> str: - """Return the id of the project domain.""" - return self.get_remote_app_data('project-domain-id') - - @property - def region(self) -> str: - """Return the region for the auth urls.""" - return self.get_remote_app_data('region') - - def request_credentials(self) -> None: - """Request credentials from the CloudCredentials server.""" - if self.model.unit.is_leader(): - logging.debug(f'Requesting credentials for {self.charm.app.name}') - app_data = self._cloud_credentials_rel.data[self.charm.app] - app_data['username'] = self.charm.app.name - - -class HasCloudCredentialsClientsEvent(EventBase): - """Has CloudCredentialsClients Event.""" - - pass - - -class ReadyCloudCredentialsClientsEvent(EventBase): - """CloudCredentialsClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, username): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.username = username - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "username": self.username, - } - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.username = snapshot["username"] - - -class CloudCredentialsClientsGoneAwayEvent(EventBase): - """Has CloudCredentialsClientsGoneAwayEvent Event.""" - - pass - - -class CloudCredentialsClientEvents(ObjectEvents): - """Events class for `on`""" - - has_cloud_credentials_clients = EventSource( - HasCloudCredentialsClientsEvent - ) - ready_cloud_credentials_clients = EventSource( - ReadyCloudCredentialsClientsEvent - ) - cloud_credentials_clients_gone = EventSource( - CloudCredentialsClientsGoneAwayEvent - ) - - -class CloudCredentialsProvides(Object): - """ - CloudCredentialsProvides class - """ - - on = CloudCredentialsClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_cloud_credentials_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_cloud_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_cloud_credentials_relation_broken, - ) - - def _on_cloud_credentials_relation_joined(self, event): - """Handle CloudCredentials joined.""" - logging.debug("CloudCredentialsProvides on_joined") - self.on.has_cloud_credentials_clients.emit() - - def _on_cloud_credentials_relation_changed(self, event): - """Handle CloudCredentials changed.""" - logging.debug("CloudCredentials on_changed") - REQUIRED_KEYS = ['username'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - username = event.relation.data[event.relation.app]['username'] - self.on.ready_cloud_credentials_clients.emit( - event.relation.id, - event.relation.name, - username, - ) - - def _on_cloud_credentials_relation_broken(self, event): - """Handle CloudCredentials broken.""" - logging.debug("CloudCredentialsProvides on_departed") - self.on.cloud_credentials_clients_gone.emit() - - def set_cloud_credentials(self, relation_name: int, - relation_id: str, - api_version: str, - auth_host: str, - auth_port: str, - auth_protocol: str, - internal_host: str, - internal_port: str, - internal_protocol: str, - username: str, - password: str, - project_name: str, - project_id: str, - user_domain_name: str, - user_domain_id: str, - project_domain_name: str, - project_domain_id: str, - region: str): - logging.debug("Setting cloud_credentials connection information.") - _cloud_credentials_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _cloud_credentials_rel = relation - if not _cloud_credentials_rel: - # Relation has disappeared so don't send the data - return - app_data = _cloud_credentials_rel.data[self.charm.app] - app_data["api-version"] = api_version - app_data["auth-host"] = auth_host - app_data["auth-port"] = str(auth_port) - app_data["auth-protocol"] = auth_protocol - app_data["internal-host"] = internal_host - app_data["internal-port"] = str(internal_port) - app_data["internal-protocol"] = internal_protocol - app_data["username"] = username - app_data["password"] = password - app_data["project-name"] = project_name - app_data["project-id"] = project_id - app_data["user-domain-name"] = user_domain_name - app_data["user-domain-id"] = user_domain_id - app_data["project-domain-name"] = project_domain_name - app_data["project-domain-id"] = project_domain_id - app_data["region"] = region diff --git a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/identity_credentials.py b/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/identity_credentials.py deleted file mode 100644 index e3f4565d..00000000 --- a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/identity_credentials.py +++ /dev/null @@ -1,458 +0,0 @@ -"""IdentityCredentialsProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_credentials interface. - -Import `IdentityCredentialsRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_credentials" - -Also provide additional parameters to the charm object: - - service - - internal_url - - public_url - - admin_url - - region - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v0.identity_credentials import IdentityCredentialsRequires - -class IdentityCredentialsClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityCredentials Requires - self.identity_credentials = IdentityCredentialsRequires( - self, "identity_credentials", - service = "my-service" - internal_url = "http://internal-url" - public_url = "http://public-url" - admin_url = "http://admin-url" - region = "region" - ) - self.framework.observe( - self.identity_credentials.on.connected, self._on_identity_credentials_connected) - self.framework.observe( - self.identity_credentials.on.ready, self._on_identity_credentials_ready) - self.framework.observe( - self.identity_credentials.on.goneaway, self._on_identity_credentials_goneaway) - - def _on_identity_credentials_connected(self, event): - '''React to the IdentityCredentials connected event. - - This event happens when IdentityCredentials relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_credentials_ready(self, event): - '''React to the IdentityCredentials ready event. - - The IdentityCredentials interface will use the provided config for the - request to the identity server. - ''' - # IdentityCredentials Relation is ready. Do something with the completed relation. - pass - - def _on_identity_credentials_goneaway(self, event): - '''React to the IdentityCredentials goneaway event. - - This event happens when an IdentityCredentials relation is removed. - ''' - # IdentityCredentials Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -# The unique Charmhub library identifier, never change it -LIBID = "b5fa18d4427c4ab9a269c3a2fbed545c" - -# 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 IdentityCredentialsConnectedEvent(EventBase): - """IdentityCredentials connected Event.""" - - pass - - -class IdentityCredentialsReadyEvent(EventBase): - """IdentityCredentials ready for use Event.""" - - pass - - -class IdentityCredentialsGoneAwayEvent(EventBase): - """IdentityCredentials relation has gone-away Event""" - - pass - - -class IdentityCredentialsServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityCredentialsConnectedEvent) - ready = EventSource(IdentityCredentialsReadyEvent) - goneaway = EventSource(IdentityCredentialsGoneAwayEvent) - - -class IdentityCredentialsRequires(Object): - """ - IdentityCredentialsRequires class - """ - - on = IdentityCredentialsServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_credentials_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_credentials_relation_broken, - ) - - def _on_identity_credentials_relation_joined(self, event): - """IdentityCredentials relation joined.""" - logging.debug("IdentityCredentials on_joined") - self.on.connected.emit() - self.request_credentials() - - def _on_identity_credentials_relation_changed(self, event): - """IdentityCredentials relation changed.""" - logging.debug("IdentityCredentials on_changed") - try: - self.on.ready.emit() - except (AttributeError, KeyError): - logger.exception('Error when emitting event') - - def _on_identity_credentials_relation_broken(self, event): - """IdentityCredentials relation broken.""" - logging.debug("IdentityCredentials on_broken") - self.on.goneaway.emit() - - @property - def _identity_credentials_rel(self) -> Relation: - """The IdentityCredentials relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._identity_credentials_rel.data[self._identity_credentials_rel.app] - return data.get(key) - - @property - def api_version(self) -> str: - """Return the api_version.""" - return self.get_remote_app_data('api-version') - - @property - def auth_host(self) -> str: - """Return the auth_host.""" - return self.get_remote_app_data('auth-host') - - @property - def auth_port(self) -> str: - """Return the auth_port.""" - return self.get_remote_app_data('auth-port') - - @property - def auth_protocol(self) -> str: - """Return the auth_protocol.""" - return self.get_remote_app_data('auth-protocol') - - @property - def internal_host(self) -> str: - """Return the internal_host.""" - return self.get_remote_app_data('internal-host') - - @property - def internal_port(self) -> str: - """Return the internal_port.""" - return self.get_remote_app_data('internal-port') - - @property - def internal_protocol(self) -> str: - """Return the internal_protocol.""" - return self.get_remote_app_data('internal-protocol') - - @property - def credentials(self) -> str: - return self.get_remote_app_data('credentials') - - @property - def username(self) -> str: - credentials_id = self.get_remote_app_data('credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("username") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def password(self) -> str: - credentials_id = self.get_remote_app_data('credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("password") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def project_name(self) -> str: - """Return the project name.""" - return self.get_remote_app_data('project-name') - - @property - def project_id(self) -> str: - """Return the project id.""" - return self.get_remote_app_data('project-id') - - @property - def user_domain_name(self) -> str: - """Return the name of the user domain.""" - return self.get_remote_app_data('user-domain-name') - - @property - def user_domain_id(self) -> str: - """Return the id of the user domain.""" - return self.get_remote_app_data('user-domain-id') - - @property - def project_domain_name(self) -> str: - """Return the name of the project domain.""" - return self.get_remote_app_data('project-domain-name') - - @property - def project_domain_id(self) -> str: - """Return the id of the project domain.""" - return self.get_remote_app_data('project-domain-id') - - @property - def region(self) -> str: - """Return the region for the auth urls.""" - return self.get_remote_app_data('region') - - @property - def internal_endpoint(self) -> str: - """Return the region for the internal auth url.""" - return self.get_remote_app_data('internal-endpoint') - - @property - def public_endpoint(self) -> str: - """Return the region for the public auth url.""" - return self.get_remote_app_data('public-endpoint') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def request_credentials(self) -> None: - """Request credentials from the IdentityCredentials server.""" - if self.model.unit.is_leader(): - logging.debug(f'Requesting credentials for {self.charm.app.name}') - app_data = self._identity_credentials_rel.data[self.charm.app] - app_data['username'] = self.charm.app.name - - -class HasIdentityCredentialsClientsEvent(EventBase): - """Has IdentityCredentialsClients Event.""" - - pass - - -class ReadyIdentityCredentialsClientsEvent(EventBase): - """IdentityCredentialsClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, username): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.username = username - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "username": self.username, - } - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.username = snapshot["username"] - - -class IdentityCredentialsClientsGoneAwayEvent(EventBase): - """Has IdentityCredentialsClientsGoneAwayEvent Event.""" - - pass - - -class IdentityCredentialsClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_credentials_clients = EventSource( - HasIdentityCredentialsClientsEvent - ) - ready_identity_credentials_clients = EventSource( - ReadyIdentityCredentialsClientsEvent - ) - identity_credentials_clients_gone = EventSource( - IdentityCredentialsClientsGoneAwayEvent - ) - - -class IdentityCredentialsProvides(Object): - """ - IdentityCredentialsProvides class - """ - - on = IdentityCredentialsClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_credentials_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_credentials_relation_broken, - ) - - def _on_identity_credentials_relation_joined(self, event): - """Handle IdentityCredentials joined.""" - logging.debug("IdentityCredentialsProvides on_joined") - self.on.has_identity_credentials_clients.emit() - - def _on_identity_credentials_relation_changed(self, event): - """Handle IdentityCredentials changed.""" - logging.debug("IdentityCredentials on_changed") - REQUIRED_KEYS = ['username'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - username = event.relation.data[event.relation.app]['username'] - self.on.ready_identity_credentials_clients.emit( - event.relation.id, - event.relation.name, - username, - ) - - def _on_identity_credentials_relation_broken(self, event): - """Handle IdentityCredentials broken.""" - logging.debug("IdentityCredentialsProvides on_departed") - self.on.identity_credentials_clients_gone.emit() - - def set_identity_credentials(self, relation_name: int, - relation_id: str, - api_version: str, - auth_host: str, - auth_port: str, - auth_protocol: str, - internal_host: str, - internal_port: str, - internal_protocol: str, - credentials: str, - project_name: str, - project_id: str, - user_domain_name: str, - user_domain_id: str, - project_domain_name: str, - project_domain_id: str, - region: str, - admin_role: str): - logging.debug("Setting identity_credentials connection information.") - _identity_credentials_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_credentials_rel = relation - if not _identity_credentials_rel: - # Relation has disappeared so don't send the data - return - app_data = _identity_credentials_rel.data[self.charm.app] - app_data["api-version"] = api_version - app_data["auth-host"] = auth_host - app_data["auth-port"] = str(auth_port) - app_data["auth-protocol"] = auth_protocol - app_data["internal-host"] = internal_host - app_data["internal-port"] = str(internal_port) - app_data["internal-protocol"] = internal_protocol - app_data["credentials"] = credentials - app_data["project-name"] = project_name - app_data["project-id"] = project_id - app_data["user-domain-name"] = user_domain_name - app_data["user-domain-id"] = user_domain_id - app_data["project-domain-name"] = project_domain_name - app_data["project-domain-id"] = project_domain_id - app_data["region"] = region - app_data["internal-endpoint"] = self.charm.internal_endpoint - app_data["public-endpoint"] = self.charm.public_endpoint - app_data["admin-role"] = admin_role diff --git a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/identity_service.py b/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/identity_service.py deleted file mode 100644 index e8d2773e..00000000 --- a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v0/identity_service.py +++ /dev/null @@ -1,493 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -Also provide additional parameters to the charm object: - - service - - internal_url - - public_url - - admin_url - - region - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v0.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - service = "my-service" - internal_url = "http://internal-url" - public_url = "http://public-url" - admin_url = "http://admin-url" - region = "region" - ) - self.framework.observe( - self.identity_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import Relation - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# 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 = 2 - - -logger = logging.getLogger(__name__) - - -class IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._identity_service_rel.data[self._identity_service_rel.app] - return data.get(key) - - @property - def api_version(self) -> str: - """Return the api_version.""" - return self.get_remote_app_data('api-version') - - @property - def auth_host(self) -> str: - """Return the auth_host.""" - return self.get_remote_app_data('auth-host') - - @property - def auth_port(self) -> str: - """Return the auth_port.""" - return self.get_remote_app_data('auth-port') - - @property - def auth_protocol(self) -> str: - """Return the auth_protocol.""" - return self.get_remote_app_data('auth-protocol') - - @property - def internal_host(self) -> str: - """Return the internal_host.""" - return self.get_remote_app_data('internal-host') - - @property - def internal_port(self) -> str: - """Return the internal_port.""" - return self.get_remote_app_data('internal-port') - - @property - def internal_protocol(self) -> str: - """Return the internal_protocol.""" - return self.get_remote_app_data('internal-protocol') - - @property - def admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_password(self) -> str: - """Return the service_password.""" - return self.get_remote_app_data('service-password') - - @property - def service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - return self.get_remote_app_data('service-user-name') - - @property - def service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_credentials(self, relation_name: int, - relation_id: str, - api_version: str, - auth_host: str, - auth_port: str, - auth_protocol: str, - internal_host: str, - internal_port: str, - internal_protocol: str, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_password: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_rel.data[self.charm.app] - app_data["api-version"] = api_version - app_data["auth-host"] = auth_host - app_data["auth-port"] = str(auth_port) - app_data["auth-protocol"] = auth_protocol - app_data["internal-host"] = internal_host - app_data["internal-port"] = str(internal_port) - app_data["internal-protocol"] = internal_protocol - app_data["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-name"] = service_user.name - app_data["service-user-id"] = service_user.id - app_data["service-password"] = service_password - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url diff --git a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v1/cloud_credentials.py b/charms/openstack-hypervisor/lib/charms/keystone_k8s/v1/cloud_credentials.py deleted file mode 100644 index 75ece456..00000000 --- a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v1/cloud_credentials.py +++ /dev/null @@ -1,451 +0,0 @@ -"""CloudCredentialsProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the cloud_credentials interface. - -Import `CloudCredentialsRequires` in your charm, with the charm object and the -relation name: - - self - - "cloud_credentials" - -Also provide additional parameters to the charm object: - - service - - internal_url - - public_url - - admin_url - - region - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v0.cloud_credentials import CloudCredentialsRequires - -class CloudCredentialsClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # CloudCredentials Requires - self.cloud_credentials = CloudCredentialsRequires( - self, "cloud_credentials", - service = "my-service" - internal_url = "http://internal-url" - public_url = "http://public-url" - admin_url = "http://admin-url" - region = "region" - ) - self.framework.observe( - self.cloud_credentials.on.connected, self._on_cloud_credentials_connected) - self.framework.observe( - self.cloud_credentials.on.ready, self._on_cloud_credentials_ready) - self.framework.observe( - self.cloud_credentials.on.goneaway, self._on_cloud_credentials_goneaway) - - def _on_cloud_credentials_connected(self, event): - '''React to the CloudCredentials connected event. - - This event happens when n CloudCredentials relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_cloud_credentials_ready(self, event): - '''React to the CloudCredentials ready event. - - The CloudCredentials interface will use the provided config for the - request to the identity server. - ''' - # CloudCredentials Relation is ready. Do something with the completed relation. - pass - - def _on_cloud_credentials_goneaway(self, event): - '''React to the CloudCredentials goneaway event. - - This event happens when an CloudCredentials relation is removed. - ''' - # CloudCredentials Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -# The unique Charmhub library identifier, never change it -LIBID = "a5d96cc2686c47eea554ce2210c2d24e" - -# 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 - -logger = logging.getLogger(__name__) - - -class CloudCredentialsConnectedEvent(EventBase): - """CloudCredentials connected Event.""" - - pass - - -class CloudCredentialsReadyEvent(EventBase): - """CloudCredentials ready for use Event.""" - - pass - - -class CloudCredentialsGoneAwayEvent(EventBase): - """CloudCredentials relation has gone-away Event""" - - pass - - -class CloudCredentialsServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(CloudCredentialsConnectedEvent) - ready = EventSource(CloudCredentialsReadyEvent) - goneaway = EventSource(CloudCredentialsGoneAwayEvent) - - -class CloudCredentialsRequires(Object): - """ - CloudCredentialsRequires class - """ - - on = CloudCredentialsServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_cloud_credentials_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_cloud_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_cloud_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_cloud_credentials_relation_broken, - ) - - def _on_cloud_credentials_relation_joined(self, event): - """CloudCredentials relation joined.""" - logging.debug("CloudCredentials on_joined") - self.on.connected.emit() - self.request_credentials() - - def _on_cloud_credentials_relation_changed(self, event): - """CloudCredentials relation changed.""" - logging.debug("CloudCredentials on_changed") - try: - self.on.ready.emit() - except (AttributeError, KeyError): - logger.exception('Error when emitting event') - - def _on_cloud_credentials_relation_broken(self, event): - """CloudCredentials relation broken.""" - logging.debug("CloudCredentials on_broken") - self.on.goneaway.emit() - - @property - def _cloud_credentials_rel(self) -> Relation: - """The CloudCredentials relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._cloud_credentials_rel.data[self._cloud_credentials_rel.app] - return data.get(key) - - @property - def api_version(self) -> str: - """Return the api_version.""" - return self.get_remote_app_data('api-version') - - @property - def auth_host(self) -> str: - """Return the auth_host.""" - return self.get_remote_app_data('auth-host') - - @property - def auth_port(self) -> str: - """Return the auth_port.""" - return self.get_remote_app_data('auth-port') - - @property - def auth_protocol(self) -> str: - """Return the auth_protocol.""" - return self.get_remote_app_data('auth-protocol') - - @property - def internal_host(self) -> str: - """Return the internal_host.""" - return self.get_remote_app_data('internal-host') - - @property - def internal_port(self) -> str: - """Return the internal_port.""" - return self.get_remote_app_data('internal-port') - - @property - def internal_protocol(self) -> str: - """Return the internal_protocol.""" - return self.get_remote_app_data('internal-protocol') - - @property - def credentials(self) -> str: - return self.get_remote_app_data('credentials') - - @property - def username(self) -> str: - credentials_id = self.get_remote_app_data('credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("username") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def password(self) -> str: - credentials_id = self.get_remote_app_data('credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("password") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def project_name(self) -> str: - """Return the project name.""" - return self.get_remote_app_data('project-name') - - @property - def project_id(self) -> str: - """Return the project id.""" - return self.get_remote_app_data('project-id') - - @property - def user_domain_name(self) -> str: - """Return the name of the user domain.""" - return self.get_remote_app_data('user-domain-name') - - @property - def user_domain_id(self) -> str: - """Return the id of the user domain.""" - return self.get_remote_app_data('user-domain-id') - - @property - def project_domain_name(self) -> str: - """Return the name of the project domain.""" - return self.get_remote_app_data('project-domain-name') - - @property - def project_domain_id(self) -> str: - """Return the id of the project domain.""" - return self.get_remote_app_data('project-domain-id') - - @property - def region(self) -> str: - """Return the region for the auth urls.""" - return self.get_remote_app_data('region') - - @property - def internal_endpoint(self) -> str: - """Return the region for the internal auth url.""" - return self.get_remote_app_data('internal-endpoint') - - @property - def public_endpoint(self) -> str: - """Return the region for the public auth url.""" - return self.get_remote_app_data('public-endpoint') - - def request_credentials(self) -> None: - """Request credentials from the CloudCredentials server.""" - if self.model.unit.is_leader(): - logging.debug(f'Requesting credentials for {self.charm.app.name}') - app_data = self._cloud_credentials_rel.data[self.charm.app] - app_data['username'] = self.charm.app.name - - -class HasCloudCredentialsClientsEvent(EventBase): - """Has CloudCredentialsClients Event.""" - - pass - - -class ReadyCloudCredentialsClientsEvent(EventBase): - """CloudCredentialsClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, username): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.username = username - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "username": self.username, - } - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.username = snapshot["username"] - - -class CloudCredentialsClientsGoneAwayEvent(EventBase): - """Has CloudCredentialsClientsGoneAwayEvent Event.""" - - pass - - -class CloudCredentialsClientEvents(ObjectEvents): - """Events class for `on`""" - - has_cloud_credentials_clients = EventSource( - HasCloudCredentialsClientsEvent - ) - ready_cloud_credentials_clients = EventSource( - ReadyCloudCredentialsClientsEvent - ) - cloud_credentials_clients_gone = EventSource( - CloudCredentialsClientsGoneAwayEvent - ) - - -class CloudCredentialsProvides(Object): - """ - CloudCredentialsProvides class - """ - - on = CloudCredentialsClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_cloud_credentials_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_cloud_credentials_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_cloud_credentials_relation_broken, - ) - - def _on_cloud_credentials_relation_joined(self, event): - """Handle CloudCredentials joined.""" - logging.debug("CloudCredentialsProvides on_joined") - self.on.has_cloud_credentials_clients.emit() - - def _on_cloud_credentials_relation_changed(self, event): - """Handle CloudCredentials changed.""" - logging.debug("CloudCredentials on_changed") - REQUIRED_KEYS = ['username'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - username = event.relation.data[event.relation.app]['username'] - self.on.ready_cloud_credentials_clients.emit( - event.relation.id, - event.relation.name, - username, - ) - - def _on_cloud_credentials_relation_broken(self, event): - """Handle CloudCredentials broken.""" - logging.debug("CloudCredentialsProvides on_departed") - self.on.cloud_credentials_clients_gone.emit() - - def set_cloud_credentials(self, relation_name: int, - relation_id: str, - api_version: str, - auth_host: str, - auth_port: str, - auth_protocol: str, - internal_host: str, - internal_port: str, - internal_protocol: str, - credentials: str, - project_name: str, - project_id: str, - user_domain_name: str, - user_domain_id: str, - project_domain_name: str, - project_domain_id: str, - region: str): - logging.debug("Setting cloud_credentials connection information.") - _cloud_credentials_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _cloud_credentials_rel = relation - if not _cloud_credentials_rel: - # Relation has disappeared so don't send the data - return - app_data = _cloud_credentials_rel.data[self.charm.app] - app_data["api-version"] = api_version - app_data["auth-host"] = auth_host - app_data["auth-port"] = str(auth_port) - app_data["auth-protocol"] = auth_protocol - app_data["internal-host"] = internal_host - app_data["internal-port"] = str(internal_port) - app_data["internal-protocol"] = internal_protocol - app_data["credentials"] = credentials - app_data["project-name"] = project_name - app_data["project-id"] = project_id - app_data["user-domain-name"] = user_domain_name - app_data["user-domain-id"] = user_domain_id - app_data["project-domain-name"] = project_domain_name - app_data["project-domain-id"] = project_domain_id - app_data["region"] = region - app_data["internal-endpoint"] = self.charm.internal_endpoint - app_data["public-endpoint"] = self.charm.public_endpoint diff --git a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v1/identity_service.py b/charms/openstack-hypervisor/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 35556622..00000000 --- a/charms/openstack-hypervisor/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,518 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -Also provide additional parameters to the charm object: - - service - - internal_url - - public_url - - admin_url - - region - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - service = "my-service" - internal_url = "http://internal-url" - public_url = "http://public-url" - admin_url = "http://admin-url" - region = "region" - ) - self.framework.observe( - self.identity_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# 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 = 0 - - -logger = logging.getLogger(__name__) - - -class IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._identity_service_rel.data[self._identity_service_rel.app] - return data.get(key) - - @property - def api_version(self) -> str: - """Return the api_version.""" - return self.get_remote_app_data('api-version') - - @property - def auth_host(self) -> str: - """Return the auth_host.""" - return self.get_remote_app_data('auth-host') - - @property - def auth_port(self) -> str: - """Return the auth_port.""" - return self.get_remote_app_data('auth-port') - - @property - def auth_protocol(self) -> str: - """Return the auth_protocol.""" - return self.get_remote_app_data('auth-protocol') - - @property - def internal_host(self) -> str: - """Return the internal_host.""" - return self.get_remote_app_data('internal-host') - - @property - def internal_port(self) -> str: - """Return the internal_port.""" - return self.get_remote_app_data('internal-port') - - @property - def internal_protocol(self) -> str: - """Return the internal_protocol.""" - return self.get_remote_app_data('internal-protocol') - - @property - def admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("password") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("username") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_credentials(self, relation_name: int, - relation_id: str, - api_version: str, - auth_host: str, - auth_port: str, - auth_protocol: str, - internal_host: str, - internal_port: str, - internal_protocol: str, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_rel.data[self.charm.app] - app_data["api-version"] = api_version - app_data["auth-host"] = auth_host - app_data["auth-port"] = str(auth_port) - app_data["auth-protocol"] = auth_protocol - app_data["internal-host"] = internal_host - app_data["internal-port"] = str(internal_port) - app_data["internal-protocol"] = internal_protocol - app_data["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials diff --git a/charms/openstack-hypervisor/lib/charms/observability_libs/v0/kubernetes_service_patch.py b/charms/openstack-hypervisor/lib/charms/observability_libs/v0/kubernetes_service_patch.py deleted file mode 100644 index a3fb9109..00000000 --- a/charms/openstack-hypervisor/lib/charms/observability_libs/v0/kubernetes_service_patch.py +++ /dev/null @@ -1,280 +0,0 @@ -# Copyright 2021 Canonical Ltd. -# See LICENSE file for licensing details. - -"""# 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 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 = 6 - -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", - 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 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. - 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: Sequence[PortDefinition], - 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 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. - 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=[ - 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: - 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/charms/openstack-hypervisor/lib/charms/observability_libs/v1/kubernetes_service_patch.py b/charms/openstack-hypervisor/lib/charms/observability_libs/v1/kubernetes_service_patch.py deleted file mode 100644 index 56cca01a..00000000 --- a/charms/openstack-hypervisor/lib/charms/observability_libs/v1/kubernetes_service_patch.py +++ /dev/null @@ -1,342 +0,0 @@ -# Copyright 2021 Canonical Ltd. -# See LICENSE file for licensing details. - -"""# 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.v1.kubernetes_service_patch -cat << EOF >> requirements.txt -lightkube -lightkube-models -EOF -``` - -Then, to initialise the library: - -For `ClusterIP` services: - -```python -# ... -from charms.observability_libs.v1.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.v1.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.v1.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]) - # ... -``` - -Bound with custom events by providing `refresh_event` argument: -For example, you would like to have a configurable port in your charm and want to apply -service patch every time charm config is changed. - -```python -from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch -from lightkube.models.core_v1 import ServicePort - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - port = ServicePort(int(self.config["charm-config-port"]), name=f"{self.app.name}") - self.service_patcher = KubernetesServicePatch( - self, - [port], - refresh_event=self.on.config_changed - ) - # ... -``` - -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, Optional, Union - -from lightkube import ApiError, Client -from lightkube.core import exceptions -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 BoundEvent, 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 = 6 - -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: Optional[str] = None, - service_type: ServiceType = "ClusterIP", - additional_labels: Optional[dict] = None, - additional_selectors: Optional[dict] = None, - additional_annotations: Optional[dict] = None, - *, - refresh_event: Optional[Union[BoundEvent, List[BoundEvent]]] = 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. - refresh_event: an optional bound event or list of bound events which - will be observed to re-apply the patch (e.g. on port change). - The `install` and `upgrade-charm` events would be observed regardless. - """ - 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) - self.framework.observe(charm.on.update_status, self._patch) - - # apply user defined events - if refresh_event: - if not isinstance(refresh_event, list): - refresh_event = [refresh_event] - - for evt in refresh_event: - self.framework.observe(evt, self._patch) - - def _service_object( - self, - ports: List[ServicePort], - service_name: Optional[str] = None, - service_type: ServiceType = "ClusterIP", - additional_labels: Optional[dict] = None, - additional_selectors: Optional[dict] = None, - additional_annotations: Optional[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. - """ - try: - client = Client() - except exceptions.ConfigError as e: - logger.warning("Error creating k8s client: %s", e) - return - - try: - if self._is_patched(client): - return - 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() - return self._is_patched(client) - - def _is_patched(self, client: Client) -> bool: - # Get the relevant service from the cluster - try: - service = client.get(Service, name=self.service_name, namespace=self._namespace) - except ApiError as e: - if e.status.code == 404 and self.service_name != self._app: - return False - else: - logger.error("Kubernetes service get failed: %s", str(e)) - raise - - # 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/charms/openstack-hypervisor/lib/charms/ovn_central_k8s/v0/ovsdb.py b/charms/openstack-hypervisor/lib/charms/ovn_central_k8s/v0/ovsdb.py deleted file mode 100644 index ef016e23..00000000 --- a/charms/openstack-hypervisor/lib/charms/ovn_central_k8s/v0/ovsdb.py +++ /dev/null @@ -1,218 +0,0 @@ -"""TODO: Add a proper docstring here. - -This is a placeholder docstring for this charm library. Docstrings are -presented on Charmhub and updated whenever you push a new version of the -library. - -Complete documentation about creating and documenting libraries can be found -in the SDK docs at https://juju.is/docs/sdk/libraries. - -See `charmcraft publish-lib` and `charmcraft fetch-lib` for details of how to -share and consume charm libraries. They serve to enhance collaboration -between charmers. Use a charmer's libraries for classes that handle -integration with their charm. - -Bear in mind that new revisions of the different major API versions (v0, v1, -v2 etc) are maintained independently. You can continue to update v0 and v1 -after you have pushed v3. - -Markdown is supported, following the CommonMark specification. -""" - -import logging -import typing -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -# The unique Charmhub library identifier, never change it -LIBID = "114b7bb1970445daa61650e451f9da62" - -# Increment this major API version when introducing breaking changes -LIBAPI = 0 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 3 - - -# TODO: add your code here! Happy coding! -class OVSDBCMSConnectedEvent(EventBase): - """OVSDBCMS connected Event.""" - - pass - - -class OVSDBCMSReadyEvent(EventBase): - """OVSDBCMS ready for use Event.""" - - pass - - -class OVSDBCMSGoneAwayEvent(EventBase): - """OVSDBCMS relation has gone-away Event""" - - pass - - -class OVSDBCMSServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(OVSDBCMSConnectedEvent) - ready = EventSource(OVSDBCMSReadyEvent) - goneaway = EventSource(OVSDBCMSGoneAwayEvent) - - -class OVSDBCMSRequires(Object): - """ - OVSDBCMSRequires class - """ - - on = OVSDBCMSServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_ovsdb_cms_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_ovsdb_cms_relation_broken, - ) - - def _on_ovsdb_cms_relation_joined(self, event): - """OVSDBCMS relation joined.""" - logging.debug("OVSDBCMSRequires on_joined") - self.on.connected.emit() - - def bound_hostnames(self): - return self.get_all_unit_values("bound-hostname") - - def bound_addresses(self): - return self.get_all_unit_values("bound-address") - - def public_address(self): - relation = self.framework.model.get_relation(self.relation_name) - data = relation.data[relation.app] - return data.get('public-address') - - def remote_ready(self): - return all(self.bound_hostnames()) or all(self.bound_addresses()) - - def _on_ovsdb_cms_relation_changed(self, event): - """OVSDBCMS relation changed.""" - logging.debug("OVSDBCMSRequires on_changed") - if self.remote_ready(): - self.on.ready.emit() - - def _on_ovsdb_cms_relation_broken(self, event): - """OVSDBCMS relation broken.""" - logging.debug("OVSDBCMSRequires on_broken") - self.on.goneaway.emit() - - def get_all_unit_values(self, key: str) -> typing.List[str]: - """Retrieve value for key from all related units.""" - values = [] - relation = self.framework.model.get_relation(self.relation_name) - if relation: - for unit in relation.units: - values.append(relation.data[unit].get(key)) - return values - - - -class OVSDBCMSClientConnectedEvent(EventBase): - """OVSDBCMS connected Event.""" - - pass - - -class OVSDBCMSClientReadyEvent(EventBase): - """OVSDBCMS ready for use Event.""" - - pass - - -class OVSDBCMSClientGoneAwayEvent(EventBase): - """OVSDBCMS relation has gone-away Event""" - - pass - - -class OVSDBCMSClientEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(OVSDBCMSClientConnectedEvent) - ready = EventSource(OVSDBCMSClientReadyEvent) - goneaway = EventSource(OVSDBCMSClientGoneAwayEvent) - - -class OVSDBCMSProvides(Object): - """ - OVSDBCMSProvides class - """ - - on = OVSDBCMSClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_ovsdb_cms_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_ovsdb_cms_relation_broken, - ) - - def _on_ovsdb_cms_relation_joined(self, event): - """Handle ovsdb-cms joined.""" - logging.debug("OVSDBCMSProvides on_joined") - self.on.connected.emit() - - def _on_ovsdb_cms_relation_changed(self, event): - """Handle ovsdb-cms changed.""" - logging.debug("OVSDBCMSProvides on_changed") - self.on.ready.emit() - - def _on_ovsdb_cms_relation_broken(self, event): - """Handle ovsdb-cms broken.""" - logging.debug("OVSDBCMSProvides on_departed") - self.on.goneaway.emit() - - def set_unit_data(self, settings: typing.Dict[str, str]) -> None: - """Publish settings on the peer unit data bag.""" - relations = self.framework.model.relations[self.relation_name] - for relation in relations: - for k, v in settings.items(): - relation.data[self.model.unit][k] = v - - def set_app_data(self, settings: typing.Dict[str, str]) -> None: - """Publish settings on the app data bag.""" - relations = self.framework.model.relations[self.relation_name] - for relation in relations: - for k, v in settings.items(): - relation.data[self.charm.app][k] = v diff --git a/charms/openstack-hypervisor/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/openstack-hypervisor/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/openstack-hypervisor/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/openstack-hypervisor/lib/charms/tls_certificates_interface/v1/tls_certificates.py b/charms/openstack-hypervisor/lib/charms/tls_certificates_interface/v1/tls_certificates.py deleted file mode 100644 index 1eda19bf..00000000 --- a/charms/openstack-hypervisor/lib/charms/tls_certificates_interface/v1/tls_certificates.py +++ /dev/null @@ -1,1261 +0,0 @@ -# Copyright 2021 Canonical Ltd. -# See LICENSE file for licensing details. - -"""Library for the tls-certificates relation. - -This library contains the Requires and Provides classes for handling the tls-certificates -interface. - -## Getting Started -From a charm directory, fetch the library using `charmcraft`: - -```shell -charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates -``` - -Add the following libraries to the charm's `requirements.txt` file: -- jsonschema -- cryptography - -Add the following section to the charm's `charmcraft.yaml` file: -```yaml -parts: - charm: - build-packages: - - libffi-dev - - libssl-dev - - rustc - - cargo -``` - -### Provider charm -The provider charm is the charm providing certificates to another charm that requires them. In -this example, the provider charm is storing its private key using a peer relation interface called -`replicas`. - -Example: -```python -from charms.tls_certificates_interface.v1.tls_certificates import ( - CertificateCreationRequestEvent, - CertificateRevocationRequestEvent, - TLSCertificatesProvidesV1, - generate_private_key, -) -from ops.charm import CharmBase, InstallEvent -from ops.main import main -from ops.model import ActiveStatus, WaitingStatus - - -def generate_ca(private_key: bytes, subject: str) -> str: - return "whatever ca content" - - -def generate_certificate(ca: str, private_key: str, csr: str) -> str: - return "Whatever certificate" - - -class ExampleProviderCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - self.certificates = TLSCertificatesProvidesV1(self, "certificates") - self.framework.observe( - self.certificates.on.certificate_request, self._on_certificate_request - ) - self.framework.observe( - self.certificates.on.certificate_revoked, self._on_certificate_revocation_request - ) - self.framework.observe(self.on.install, self._on_install) - - def _on_install(self, event: InstallEvent) -> None: - private_key_password = b"banana" - private_key = generate_private_key(password=private_key_password) - ca_certificate = generate_ca(private_key=private_key, subject="whatever") - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update( - { - "private_key_password": "banana", - "private_key": private_key, - "ca_certificate": ca_certificate, - } - ) - self.unit.status = ActiveStatus() - - def _on_certificate_request(self, event: CertificateCreationRequestEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - ca_certificate = replicas_relation.data[self.app].get("ca_certificate") - private_key = replicas_relation.data[self.app].get("private_key") - certificate = generate_certificate( - ca=ca_certificate, - private_key=private_key, - csr=event.certificate_signing_request, - ) - - self.certificates.set_relation_certificate( - certificate=certificate, - certificate_signing_request=event.certificate_signing_request, - ca=ca_certificate, - chain=[ca_certificate, certificate], - relation_id=event.relation_id, - ) - - def _on_certificate_revocation_request(self, event: CertificateRevocationRequestEvent) -> None: - # Do what you want to do with this information - pass - - -if __name__ == "__main__": - main(ExampleProviderCharm) -``` - -### Requirer charm -The requirer charm is the charm requiring certificates from another charm that provides them. In -this example, the requirer charm is storing its certificates using a peer relation interface called -`replicas`. - -Example: -```python -from charms.tls_certificates_interface.v1.tls_certificates import ( - CertificateAvailableEvent, - CertificateExpiringEvent, - TLSCertificatesRequiresV1, - generate_csr, - generate_private_key, -) -from ops.charm import CharmBase, RelationJoinedEvent -from ops.main import main -from ops.model import ActiveStatus, WaitingStatus - - -class ExampleRequirerCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - self.cert_subject = "whatever" - self.certificates = TLSCertificatesRequiresV1(self, "certificates") - self.framework.observe(self.on.install, self._on_install) - self.framework.observe( - self.on.certificates_relation_joined, self._on_certificates_relation_joined - ) - self.framework.observe( - self.certificates.on.certificate_available, self._on_certificate_available - ) - self.framework.observe( - self.certificates.on.certificate_expiring, self._on_certificate_expiring - ) - - def _on_install(self, event) -> None: - private_key_password = b"banana" - private_key = generate_private_key(password=private_key_password) - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update( - {"private_key_password": "banana", "private_key": private_key.decode()} - ) - - def _on_certificates_relation_joined(self, event: RelationJoinedEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - replicas_relation.data[self.app].update({"csr": csr.decode()}) - self.certificates.request_certificate_creation(certificate_signing_request=csr) - - def _on_certificate_available(self, event: CertificateAvailableEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update({"certificate": event.certificate}) - replicas_relation.data[self.app].update({"ca": event.ca}) - replicas_relation.data[self.app].update({"chain": event.chain}) - self.unit.status = ActiveStatus() - - def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - old_csr = replicas_relation.data[self.app].get("csr") - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - new_csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - self.certificates.request_certificate_renewal( - old_certificate_signing_request=old_csr, - new_certificate_signing_request=new_csr, - ) - replicas_relation.data[self.app].update({"csr": new_csr.decode()}) - - -if __name__ == "__main__": - main(ExampleRequirerCharm) -``` -""" # noqa: D405, D410, D411, D214, D416 - -import copy -import json -import logging -import uuid -from datetime import datetime, timedelta -from ipaddress import IPv4Address -from typing import Dict, List, Optional - -from cryptography import x509 -from cryptography.hazmat._oid import ExtensionOID -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.hazmat.primitives.serialization import pkcs12 -from cryptography.x509.extensions import Extension, ExtensionNotFound -from jsonschema import exceptions, validate # type: ignore[import] -from ops.charm import CharmBase, CharmEvents, RelationChangedEvent, UpdateStatusEvent -from ops.framework import EventBase, EventSource, Handle, Object - -# The unique Charmhub library identifier, never change it -LIBID = "afd8c2bccf834997afce12c2706d2ede" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 10 - -REQUIRER_JSON_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/requirer.json", # noqa: E501 - "type": "object", - "title": "`tls_certificates` requirer root schema", - "description": "The `tls_certificates` root schema comprises the entire requirer databag for this interface.", # noqa: E501 - "examples": [ - { - "certificate_signing_requests": [ - { - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\\n-----END CERTIFICATE REQUEST-----\\n" # noqa: E501 - }, - { - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBAMk3raaX803cHvzlBF9LC7KORT46z4VjyU5PIaMb\\nQLIDgYKFYI0n5hf2Ra4FAHvOvEmW7bjNlHORFEmvnpcU5kPMNUyKFMTaC8LGmN8z\\nUBH3aK+0+FRvY4afn9tgj5435WqOG9QdoDJ0TJkjJbJI9M70UOgL711oU7ql6HxU\\n4d2ydFK9xAHrBwziNHgNZ72L95s4gLTXf0fAHYf15mDA9U5yc+YDubCKgTXzVySQ\\nUx73VCJLfC/XkZIh559IrnRv5G9fu6BMLEuBwAz6QAO4+/XidbKWN4r2XSq5qX4n\\n6EPQQWP8/nd4myq1kbg6Q8w68L/0YdfjCmbyf2TuoWeImdUCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQBIdwraBvpYo/rl5MH1+1Um6HRg4gOdQPY5WcJy9B9tgzJz\\nittRSlRGTnhyIo6fHgq9KHrmUthNe8mMTDailKFeaqkVNVvk7l0d1/B90Kz6OfmD\\nxN0qjW53oP7y3QB5FFBM8DjqjmUnz5UePKoX4AKkDyrKWxMwGX5RoET8c/y0y9jp\\nvSq3Wh5UpaZdWbe1oVY8CqMVUEVQL2DPjtopxXFz2qACwsXkQZxWmjvZnRiP8nP8\\nbdFaEuh9Q6rZ2QdZDEtrU4AodPU3NaukFr5KlTUQt3w/cl+5//zils6G5zUWJ2pN\\ng7+t9PTvXHRkH+LnwaVnmsBFU2e05qADQbfIn7JA\\n-----END CERTIFICATE REQUEST-----\\n" # noqa: E501 - }, - ] - } - ], - "properties": { - "certificate_signing_requests": { - "type": "array", - "items": { - "type": "object", - "properties": {"certificate_signing_request": {"type": "string"}}, - "required": ["certificate_signing_request"], - }, - } - }, - "required": ["certificate_signing_requests"], - "additionalProperties": True, -} - -PROVIDER_JSON_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/provider.json", # noqa: E501 - "type": "object", - "title": "`tls_certificates` provider root schema", - "description": "The `tls_certificates` root schema comprises the entire provider databag for this interface.", # noqa: E501 - "example": [ - { - "certificates": [ - { - "ca": "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n", # noqa: E501 - "chain": [ - "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n" # noqa: E501, W505 - ], - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\n-----END CERTIFICATE REQUEST-----\n", # noqa: E501 - "certificate": "-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n", # noqa: E501 - } - ] - } - ], - "properties": { - "certificates": { - "$id": "#/properties/certificates", - "type": "array", - "items": { - "$id": "#/properties/certificates/items", - "type": "object", - "required": ["certificate_signing_request", "certificate", "ca", "chain"], - "properties": { - "certificate_signing_request": { - "$id": "#/properties/certificates/items/certificate_signing_request", - "type": "string", - }, - "certificate": { - "$id": "#/properties/certificates/items/certificate", - "type": "string", - }, - "ca": {"$id": "#/properties/certificates/items/ca", "type": "string"}, - "chain": { - "$id": "#/properties/certificates/items/chain", - "type": "array", - "items": { - "type": "string", - "$id": "#/properties/certificates/items/chain/items", - }, - }, - }, - "additionalProperties": True, - }, - } - }, - "required": ["certificates"], - "additionalProperties": True, -} - - -logger = logging.getLogger(__name__) - - -class CertificateAvailableEvent(EventBase): - """Charm Event triggered when a TLS certificate is available.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - - -class CertificateExpiringEvent(EventBase): - """Charm Event triggered when a TLS certificate is almost expired.""" - - def __init__(self, handle, certificate: str, expiry: str): - """CertificateExpiringEvent. - - Args: - handle (Handle): Juju framework handle - certificate (str): TLS Certificate - expiry (str): Datetime string reprensenting the time at which the certificate - won't be valid anymore. - """ - super().__init__(handle) - self.certificate = certificate - self.expiry = expiry - - def snapshot(self) -> dict: - """Returns snapshot.""" - return {"certificate": self.certificate, "expiry": self.expiry} - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.expiry = snapshot["expiry"] - - -class CertificateExpiredEvent(EventBase): - """Charm Event triggered when a TLS certificate is expired.""" - - def __init__(self, handle: Handle, certificate: str): - super().__init__(handle) - self.certificate = certificate - - def snapshot(self) -> dict: - """Returns snapshot.""" - return {"certificate": self.certificate} - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - - -class CertificateCreationRequestEvent(EventBase): - """Charm Event triggered when a TLS certificate is required.""" - - def __init__(self, handle: Handle, certificate_signing_request: str, relation_id: int): - super().__init__(handle) - self.certificate_signing_request = certificate_signing_request - self.relation_id = relation_id - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate_signing_request": self.certificate_signing_request, - "relation_id": self.relation_id, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.relation_id = snapshot["relation_id"] - - -class CertificateRevocationRequestEvent(EventBase): - """Charm Event triggered when a TLS certificate needs to be revoked.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: str, - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - - -def _load_relation_data(raw_relation_data: dict) -> dict: - """Loads relation data from the relation data bag. - - Json loads all data. - - Args: - raw_relation_data: Relation data from the databag - - Returns: - dict: Relation data in dict format. - """ - certificate_data = dict() - for key in raw_relation_data: - try: - certificate_data[key] = json.loads(raw_relation_data[key]) - except (json.decoder.JSONDecodeError, TypeError): - certificate_data[key] = raw_relation_data[key] - return certificate_data - - -def generate_ca( - private_key: bytes, - subject: str, - private_key_password: Optional[bytes] = None, - validity: int = 365, - country: str = "US", -) -> bytes: - """Generates a CA Certificate. - - Args: - private_key (bytes): Private key - subject (str): Certificate subject - private_key_password (bytes): Private key password - validity (int): Certificate validity time (in days) - country (str): Certificate Issuing country - - Returns: - bytes: CA Certificate. - """ - private_key_object = serialization.load_pem_private_key( - private_key, password=private_key_password - ) - subject = issuer = x509.Name( - [ - x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country), - x509.NameAttribute(x509.NameOID.COMMON_NAME, subject), - ] - ) - subject_identifier_object = x509.SubjectKeyIdentifier.from_public_key( - private_key_object.public_key() # type: ignore[arg-type] - ) - subject_identifier = key_identifier = subject_identifier_object.public_bytes() - cert = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(private_key_object.public_key()) # type: ignore[arg-type] - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.utcnow()) - .not_valid_after(datetime.utcnow() + timedelta(days=validity)) - .add_extension(x509.SubjectKeyIdentifier(digest=subject_identifier), critical=False) - .add_extension( - x509.AuthorityKeyIdentifier( - key_identifier=key_identifier, - authority_cert_issuer=None, - authority_cert_serial_number=None, - ), - critical=False, - ) - .add_extension( - x509.BasicConstraints(ca=True, path_length=None), - critical=True, - ) - .sign(private_key_object, hashes.SHA256()) # type: ignore[arg-type] - ) - return cert.public_bytes(serialization.Encoding.PEM) - - -def generate_certificate( - csr: bytes, - ca: bytes, - ca_key: bytes, - ca_key_password: Optional[bytes] = None, - validity: int = 365, - alt_names: List[str] = None, -) -> bytes: - """Generates a TLS certificate based on a CSR. - - Args: - csr (bytes): CSR - ca (bytes): CA Certificate - ca_key (bytes): CA private key - ca_key_password: CA private key password - validity (int): Certificate validity (in days) - alt_names (list): List of alt names to put on cert - prefer putting SANs in CSR - - Returns: - bytes: Certificate - """ - csr_object = x509.load_pem_x509_csr(csr) - subject = csr_object.subject - issuer = x509.load_pem_x509_certificate(ca).issuer - private_key = serialization.load_pem_private_key(ca_key, password=ca_key_password) - - certificate_builder = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(csr_object.public_key()) - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.utcnow()) - .not_valid_after(datetime.utcnow() + timedelta(days=validity)) - ) - - extensions_list = csr_object.extensions - san_ext: Optional[x509.Extension] = None - if alt_names: - full_sans_dns = alt_names.copy() - try: - loaded_san_ext = csr_object.extensions.get_extension_for_class( - x509.SubjectAlternativeName - ) - full_sans_dns.extend(loaded_san_ext.value.get_values_for_type(x509.DNSName)) - except ExtensionNotFound: - pass - finally: - san_ext = Extension( - ExtensionOID.SUBJECT_ALTERNATIVE_NAME, - False, - x509.SubjectAlternativeName([x509.DNSName(name) for name in full_sans_dns]), - ) - if not extensions_list: - extensions_list = x509.Extensions([san_ext]) - - for extension in extensions_list: - if extension.value.oid == ExtensionOID.SUBJECT_ALTERNATIVE_NAME and san_ext: - extension = san_ext - - certificate_builder = certificate_builder.add_extension( - extension.value, - critical=extension.critical, - ) - certificate_builder._version = x509.Version.v3 - cert = certificate_builder.sign(private_key, hashes.SHA256()) # type: ignore[arg-type] - return cert.public_bytes(serialization.Encoding.PEM) - - -def generate_pfx_package( - certificate: bytes, - private_key: bytes, - package_password: str, - private_key_password: Optional[bytes] = None, -) -> bytes: - """Generates a PFX package to contain the TLS certificate and private key. - - Args: - certificate (bytes): TLS certificate - private_key (bytes): Private key - package_password (str): Password to open the PFX package - private_key_password (bytes): Private key password - - Returns: - bytes: - """ - private_key_object = serialization.load_pem_private_key( - private_key, password=private_key_password - ) - certificate_object = x509.load_pem_x509_certificate(certificate) - name = certificate_object.subject.rfc4514_string() - pfx_bytes = pkcs12.serialize_key_and_certificates( - name=name.encode(), - cert=certificate_object, - key=private_key_object, # type: ignore[arg-type] - cas=None, - encryption_algorithm=serialization.BestAvailableEncryption(package_password.encode()), - ) - return pfx_bytes - - -def generate_private_key( - password: Optional[bytes] = None, - key_size: int = 2048, - public_exponent: int = 65537, -) -> bytes: - """Generates a private key. - - Args: - password (bytes): Password for decrypting the private key - key_size (int): Key size in bytes - public_exponent: Public exponent. - - Returns: - bytes: Private Key - """ - private_key = rsa.generate_private_key( - public_exponent=public_exponent, - key_size=key_size, - ) - key_bytes = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.BestAvailableEncryption(password) - if password - else serialization.NoEncryption(), - ) - return key_bytes - - -def generate_csr( - private_key: bytes, - subject: str, - add_unique_id_to_subject_name: bool = True, - organization: str = None, - email_address: str = None, - country_name: str = None, - private_key_password: Optional[bytes] = None, - sans: Optional[List[str]] = None, - sans_oid: Optional[List[str]] = None, - sans_ip: Optional[List[str]] = None, - sans_dns: Optional[List[str]] = None, - additional_critical_extensions: Optional[List] = None, -) -> bytes: - """Generates a CSR using private key and subject. - - Args: - private_key (bytes): Private key - subject (str): CSR Subject. - add_unique_id_to_subject_name (bool): Whether a unique ID must be added to the CSR's - subject name. Always leave to "True" when the CSR is used to request certificates - using the tls-certificates relation. - organization (str): Name of organization. - email_address (str): Email address. - country_name (str): Country Name. - private_key_password (bytes): Private key password - sans (list): Use sans_dns - this will be deprecated in a future release - List of DNS subject alternative names (keeping it for now for backward compatibility) - sans_oid (list): List of registered ID SANs - sans_dns (list): List of DNS subject alternative names (similar to the arg: sans) - sans_ip (list): List of IP subject alternative names - additional_critical_extensions (list): List if critical additional extension objects. - Object must be a x509 ExtensionType. - - Returns: - bytes: CSR - """ - signing_key = serialization.load_pem_private_key(private_key, password=private_key_password) - subject_name = [x509.NameAttribute(x509.NameOID.COMMON_NAME, subject)] - if add_unique_id_to_subject_name: - unique_identifier = uuid.uuid4() - subject_name.append( - x509.NameAttribute(x509.NameOID.X500_UNIQUE_IDENTIFIER, str(unique_identifier)) - ) - if organization: - subject_name.append(x509.NameAttribute(x509.NameOID.ORGANIZATION_NAME, organization)) - if email_address: - subject_name.append(x509.NameAttribute(x509.NameOID.EMAIL_ADDRESS, email_address)) - if country_name: - subject_name.append(x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country_name)) - csr = x509.CertificateSigningRequestBuilder(subject_name=x509.Name(subject_name)) - - _sans: List[x509.GeneralName] = [] - if sans_oid: - _sans.extend([x509.RegisteredID(x509.ObjectIdentifier(san)) for san in sans_oid]) - if sans_ip: - _sans.extend([x509.IPAddress(IPv4Address(san)) for san in sans_ip]) - if sans: - _sans.extend([x509.DNSName(san) for san in sans]) - if sans_dns: - _sans.extend([x509.DNSName(san) for san in sans_dns]) - if _sans: - csr = csr.add_extension(x509.SubjectAlternativeName(set(_sans)), critical=False) - - if additional_critical_extensions: - for extension in additional_critical_extensions: - csr = csr.add_extension(extension, critical=True) - - signed_certificate = csr.sign(signing_key, hashes.SHA256()) # type: ignore[arg-type] - return signed_certificate.public_bytes(serialization.Encoding.PEM) - - -class CertificatesProviderCharmEvents(CharmEvents): - """List of events that the TLS Certificates provider charm can leverage.""" - - certificate_creation_request = EventSource(CertificateCreationRequestEvent) - certificate_revocation_request = EventSource(CertificateRevocationRequestEvent) - - -class CertificatesRequirerCharmEvents(CharmEvents): - """List of events that the TLS Certificates requirer charm can leverage.""" - - certificate_available = EventSource(CertificateAvailableEvent) - certificate_expiring = EventSource(CertificateExpiringEvent) - certificate_expired = EventSource(CertificateExpiredEvent) - - -class TLSCertificatesProvidesV1(Object): - """TLS certificates provider class to be instantiated by TLS certificates providers.""" - - on = CertificatesProviderCharmEvents() - - def __init__(self, charm: CharmBase, relationship_name: str): - super().__init__(charm, relationship_name) - self.framework.observe( - charm.on[relationship_name].relation_changed, self._on_relation_changed - ) - self.charm = charm - self.relationship_name = relationship_name - - def _add_certificate( - self, - relation_id: int, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - ) -> None: - """Adds certificate to relation data. - - Args: - relation_id (int): Relation id - certificate (str): Certificate - certificate_signing_request (str): Certificate Signing Request - ca (str): CA Certificate - chain (list): CA Chain - - Returns: - None - """ - relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - new_certificate = { - "certificate": certificate, - "certificate_signing_request": certificate_signing_request, - "ca": ca, - "chain": chain, - } - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = provider_relation_data.get("certificates", []) - certificates = copy.deepcopy(provider_certificates) - if new_certificate in certificates: - logger.info("Certificate already in relation data - Doing nothing") - return - certificates.append(new_certificate) - relation.data[self.model.app]["certificates"] = json.dumps(certificates) - - def _remove_certificate( - self, - relation_id: int, - certificate: str = None, - certificate_signing_request: str = None, - ) -> None: - """Removes certificate from a given relation based on user provided certificate or csr. - - Args: - relation_id (int): Relation id - certificate (str): Certificate (optional) - certificate_signing_request: Certificate signing request (optional) - - Returns: - None - """ - relation = self.model.get_relation( - relation_name=self.relationship_name, - relation_id=relation_id, - ) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} with relation id {relation_id} does not exist" - ) - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = provider_relation_data.get("certificates", []) - certificates = copy.deepcopy(provider_certificates) - for certificate_dict in certificates: - if certificate and certificate_dict["certificate"] == certificate: - certificates.remove(certificate_dict) - if ( - certificate_signing_request - and certificate_dict["certificate_signing_request"] == certificate_signing_request - ): - certificates.remove(certificate_dict) - relation.data[self.model.app]["certificates"] = json.dumps(certificates) - - @staticmethod - def _relation_data_is_valid(certificates_data: dict) -> bool: - """Uses JSON schema validator to validate relation data content. - - Args: - certificates_data (dict): Certificate data dictionary as retrieved from relation data. - - Returns: - bool: True/False depending on whether the relation data follows the json schema. - """ - try: - validate(instance=certificates_data, schema=REQUIRER_JSON_SCHEMA) - return True - except exceptions.ValidationError: - return False - - def revoke_all_certificates(self) -> None: - """Revokes all certificates of this provider. - - This method is meant to be used when the Root CA has changed. - """ - for relation in self.model.relations[self.relationship_name]: - relation.data[self.model.app]["certificates"] = json.dumps([]) - - def set_relation_certificate( - self, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - relation_id: int, - ) -> None: - """Adds certificates to relation data. - - Args: - certificate (str): Certificate - certificate_signing_request (str): Certificate signing request - ca (str): CA Certificate - chain (list): CA Chain - relation_id (int): Juju relation ID - - Returns: - None - """ - certificates_relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - self._remove_certificate( - certificate_signing_request=certificate_signing_request.strip(), - relation_id=relation_id, - ) - self._add_certificate( - relation_id=relation_id, - certificate=certificate.strip(), - certificate_signing_request=certificate_signing_request.strip(), - ca=ca.strip(), - chain=[cert.strip() for cert in chain], - ) - - def remove_certificate(self, certificate: str) -> None: - """Removes a given certificate from relation data. - - Args: - certificate (str): TLS Certificate - - Returns: - None - """ - certificates_relation = self.model.relations[self.relationship_name] - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - for certificate_relation in certificates_relation: - self._remove_certificate(certificate=certificate, relation_id=certificate_relation.id) - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handler triggerred on relation changed event. - - Looks at the relation data and either emits: - - certificate request event: If the unit relation data contains a CSR for which - a certificate does not exist in the provider relation data. - - certificate revocation event: If the provider relation data contains a CSR for which - a csr does not exist in the requirer relation data. - - Args: - event: Juju event - - Returns: - None - """ - assert event.unit is not None - requirer_relation_data = _load_relation_data(event.relation.data[event.unit]) - provider_relation_data = _load_relation_data(event.relation.data[self.charm.app]) - if not self._relation_data_is_valid(requirer_relation_data): - logger.warning( - f"Relation data did not pass JSON Schema validation: {requirer_relation_data}" - ) - return - provider_certificates = provider_relation_data.get("certificates", []) - requirer_csrs = requirer_relation_data.get("certificate_signing_requests", []) - provider_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in provider_certificates - ] - requirer_unit_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in requirer_csrs - ] - for certificate_signing_request in requirer_unit_csrs: - if certificate_signing_request not in provider_csrs: - self.on.certificate_creation_request.emit( - certificate_signing_request=certificate_signing_request, - relation_id=event.relation.id, - ) - self._revoke_certificates_for_which_no_csr_exists(relation_id=event.relation.id) - - def _revoke_certificates_for_which_no_csr_exists(self, relation_id: int) -> None: - """Revokes certificates for which no unit has a CSR. - - Goes through all generated certificates and compare agains the list of CSRS for all units - of a given relationship. - - Args: - relation_id (int): Relation id - - Returns: - None - """ - certificates_relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - provider_relation_data = _load_relation_data(certificates_relation.data[self.charm.app]) - list_of_csrs: List[str] = [] - for unit in certificates_relation.units: - requirer_relation_data = _load_relation_data(certificates_relation.data[unit]) - requirer_csrs = requirer_relation_data.get("certificate_signing_requests", []) - list_of_csrs.extend(csr["certificate_signing_request"] for csr in requirer_csrs) - provider_certificates = provider_relation_data.get("certificates", []) - for certificate in provider_certificates: - if certificate["certificate_signing_request"] not in list_of_csrs: - self.on.certificate_revocation_request.emit( - certificate=certificate["certificate"], - certificate_signing_request=certificate["certificate_signing_request"], - ca=certificate["ca"], - chain=certificate["chain"], - ) - self.remove_certificate(certificate=certificate["certificate"]) - - -class TLSCertificatesRequiresV1(Object): - """TLS certificates requirer class to be instantiated by TLS certificates requirers.""" - - on = CertificatesRequirerCharmEvents() - - def __init__( - self, - charm: CharmBase, - relationship_name: str, - expiry_notification_time: int = 168, - ): - """Generates/use private key and observes relation changed event. - - Args: - charm: Charm object - relationship_name: Juju relation name - expiry_notification_time (int): Time difference between now and expiry (in hours). - Used to trigger the CertificateExpiring event. Default: 7 days. - """ - super().__init__(charm, relationship_name) - self.relationship_name = relationship_name - self.charm = charm - self.expiry_notification_time = expiry_notification_time - self.framework.observe( - charm.on[relationship_name].relation_changed, self._on_relation_changed - ) - self.framework.observe(charm.on.update_status, self._on_update_status) - - @property - def _requirer_csrs(self) -> List[Dict[str, str]]: - """Returns list of requirer CSR's from relation data.""" - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - requirer_relation_data = _load_relation_data(relation.data[self.model.unit]) - return requirer_relation_data.get("certificate_signing_requests", []) - - @property - def _provider_certificates(self) -> List[Dict[str, str]]: - """Returns list of provider CSR's from relation data.""" - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - if not relation.app: - raise RuntimeError(f"Remote app for relation {self.relationship_name} does not exist") - provider_relation_data = _load_relation_data(relation.data[relation.app]) - return provider_relation_data.get("certificates", []) - - def _add_requirer_csr(self, csr: str) -> None: - """Adds CSR to relation data. - - Args: - csr (str): Certificate Signing Request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - new_csr_dict = {"certificate_signing_request": csr} - if new_csr_dict in self._requirer_csrs: - logger.info("CSR already in relation data - Doing nothing") - return - requirer_csrs = copy.deepcopy(self._requirer_csrs) - requirer_csrs.append(new_csr_dict) - relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs) - - def _remove_requirer_csr(self, csr: str) -> None: - """Removes CSR from relation data. - - Args: - csr (str): Certificate signing request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - requirer_csrs = copy.deepcopy(self._requirer_csrs) - csr_dict = {"certificate_signing_request": csr} - if csr_dict not in requirer_csrs: - logger.info("CSR not in relation data - Doing nothing") - return - requirer_csrs.remove(csr_dict) - relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs) - - def request_certificate_creation(self, certificate_signing_request: bytes) -> None: - """Request TLS certificate to provider charm. - - Args: - certificate_signing_request (bytes): Certificate Signing Request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - message = ( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - logger.error(message) - raise RuntimeError(message) - self._add_requirer_csr(certificate_signing_request.decode().strip()) - logger.info("Certificate request sent to provider") - - def request_certificate_revocation(self, certificate_signing_request: bytes) -> None: - """Removes CSR from relation data. - - The provider of this relation is then expected to remove certificates associated to this - CSR from the relation data as well and emit a request_certificate_revocation event for the - provider charm to interpret. - - Args: - certificate_signing_request (bytes): Certificate Signing Request - - Returns: - None - """ - self._remove_requirer_csr(certificate_signing_request.decode().strip()) - logger.info("Certificate revocation sent to provider") - - def request_certificate_renewal( - self, old_certificate_signing_request: bytes, new_certificate_signing_request: bytes - ) -> None: - """Renews certificate. - - Removes old CSR from relation data and adds new one. - - Args: - old_certificate_signing_request: Old CSR - new_certificate_signing_request: New CSR - - Returns: - None - """ - try: - self.request_certificate_revocation( - certificate_signing_request=old_certificate_signing_request - ) - except RuntimeError: - logger.warning("Certificate revocation failed.") - self.request_certificate_creation( - certificate_signing_request=new_certificate_signing_request - ) - logger.info("Certificate renewal request completed.") - - @staticmethod - def _relation_data_is_valid(certificates_data: dict) -> bool: - """Checks whether relation data is valid based on json schema. - - Args: - certificates_data: Certificate data in dict format. - - Returns: - bool: Whether relation data is valid. - """ - try: - validate(instance=certificates_data, schema=PROVIDER_JSON_SCHEMA) - return True - except exceptions.ValidationError: - return False - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handler triggerred on relation changed events. - - Args: - event: Juju event - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - logger.warning(f"No relation: {self.relationship_name}") - return - if not relation.app: - logger.warning(f"No remote app in relation: {self.relationship_name}") - return - provider_relation_data = _load_relation_data(relation.data[relation.app]) - if not self._relation_data_is_valid(provider_relation_data): - logger.warning( - f"Provider relation data did not pass JSON Schema validation: " - f"{event.relation.data[relation.app]}" - ) - return - requirer_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in self._requirer_csrs - ] - for certificate in self._provider_certificates: - if certificate["certificate_signing_request"] in requirer_csrs: - self.on.certificate_available.emit( - certificate_signing_request=certificate["certificate_signing_request"], - certificate=certificate["certificate"], - ca=certificate["ca"], - chain=certificate["chain"], - ) - - def _on_update_status(self, event: UpdateStatusEvent) -> None: - """Triggered on update status event. - - Goes through each certificate in the "certificates" relation and checks their expiry date. - If they are close to expire (<7 days), emits a CertificateExpiringEvent event and if - they are expired, emits a CertificateExpiredEvent. - - Args: - event (UpdateStatusEvent): Juju event - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - logger.warning(f"No relation: {self.relationship_name}") - return - if not relation.app: - logger.warning(f"No remote app in relation: {self.relationship_name}") - return - provider_relation_data = _load_relation_data(relation.data[relation.app]) - if not self._relation_data_is_valid(provider_relation_data): - logger.warning( - f"Provider relation data did not pass JSON Schema validation: " - f"{relation.data[relation.app]}" - ) - return - for certificate_dict in self._provider_certificates: - certificate = certificate_dict["certificate"] - try: - certificate_object = x509.load_pem_x509_certificate(data=certificate.encode()) - except ValueError: - logger.warning("Could not load certificate.") - continue - time_difference = certificate_object.not_valid_after - datetime.utcnow() - if time_difference.total_seconds() < 0: - logger.warning("Certificate is expired") - self.on.certificate_expired.emit(certificate=certificate) - self.request_certificate_revocation(certificate.encode()) - continue - if time_difference.total_seconds() < (self.expiry_notification_time * 60 * 60): - logger.warning("Certificate almost expired") - self.on.certificate_expiring.emit( - certificate=certificate, expiry=certificate_object.not_valid_after.isoformat() - ) diff --git a/charms/openstack-hypervisor/lib/charms/traefik_k8s/v1/ingress.py b/charms/openstack-hypervisor/lib/charms/traefik_k8s/v1/ingress.py deleted file mode 100644 index e1769e8c..00000000 --- a/charms/openstack-hypervisor/lib/charms/traefik_k8s/v1/ingress.py +++ /dev/null @@ -1,558 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v1.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" - -import logging -import socket -import typing -from typing import Any, Dict, Optional, Tuple, Union - -import yaml -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# 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 = 5 - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) - -try: - import jsonschema - - DO_VALIDATION = True -except ModuleNotFoundError: - log.warning( - "The `ingress` library needs the `jsonschema` package to be able " - "to do runtime data validation; without it, it will still work but validation " - "will be disabled. \n" - "It is recommended to add `jsonschema` to the 'requirements.txt' of your charm, " - "which will enable this feature." - ) - DO_VALIDATION = False - -INGRESS_REQUIRES_APP_SCHEMA = { - "type": "object", - "properties": { - "model": {"type": "string"}, - "name": {"type": "string"}, - "host": {"type": "string"}, - "port": {"type": "string"}, - "strip-prefix": {"type": "string"}, - }, - "required": ["model", "name", "host", "port"], -} - -INGRESS_PROVIDES_APP_SCHEMA = { - "type": "object", - "properties": { - "ingress": {"type": "object", "properties": {"url": {"type": "string"}}}, - }, - "required": ["ingress"], -} - -try: - from typing import TypedDict -except ImportError: - from typing_extensions import TypedDict # py35 compat - -# Model of the data a unit implementing the requirer will need to provide. -RequirerData = TypedDict( - "RequirerData", - {"model": str, "name": str, "host": str, "port": int, "strip-prefix": bool}, - total=False, -) -# Provider ingress data model. -ProviderIngressData = TypedDict("ProviderIngressData", {"url": str}) -# Provider application databag model. -ProviderApplicationData = TypedDict("ProviderApplicationData", {"ingress": ProviderIngressData}) - - -def _validate_data(data, schema): - """Checks whether `data` matches `schema`. - - Will raise DataValidationError if the data is not valid, else return None. - """ - if not DO_VALIDATION: - return - try: - jsonschema.validate(instance=data, schema=schema) - except jsonschema.ValidationError as e: - raise DataValidationError(data, schema) from e - - -class DataValidationError(RuntimeError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__ = () # type: Tuple[str, ...] - __optional_kwargs__ = {} # type: Dict[str, Any] - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self) -> dict: - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot: dict) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "port", "host", "strip_prefix") - - if typing.TYPE_CHECKING: - name = None # type: str - model = None # type: str - port = None # type: int - host = None # type: str - strip_prefix = False # type: bool - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self._get_requirer_data(event.relation) - self.on.data_provided.emit( - event.relation, - data["name"], - data["model"], - data["port"], - data["host"], - data.get("strip-prefix", False), - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_data(self, relation: Relation) -> RequirerData: - """Fetch and validate the requirer's app databag. - - For convenience, we convert 'port' to integer. - """ - if not all((relation.app, relation.app.name)): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return {} - - databag = relation.data[relation.app] - remote_data = {} # type: Dict[str, Union[int, str]] - for k in ("port", "host", "model", "name", "mode", "strip-prefix"): - v = databag.get(k) - if v is not None: - remote_data[k] = v - _validate_data(remote_data, INGRESS_REQUIRES_APP_SCHEMA) - remote_data["port"] = int(remote_data["port"]) - remote_data["strip-prefix"] = bool(remote_data.get("strip-prefix", False)) - return remote_data - - def get_data(self, relation: Relation) -> RequirerData: - """Fetch the remote app's databag, i.e. the requirer data.""" - return self._get_requirer_data(relation) - - def is_ready(self, relation: Relation = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - return bool(self._get_requirer_data(relation)) - except DataValidationError as e: - log.warning("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _provided_url(self, relation: Relation) -> ProviderIngressData: - """Fetch and validate this app databag; return the ingress url.""" - if not all((relation.app, relation.app.name, self.unit.is_leader())): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return {} # noqa - - # fetch the provider's app databag - raw_data = relation.data[self.app].get("ingress") - if not raw_data: - raise RuntimeError("This application did not `publish_url` yet.") - - ingress: ProviderIngressData = yaml.safe_load(raw_data) - _validate_data({"ingress": ingress}, INGRESS_PROVIDES_APP_SCHEMA) - return ingress - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress = {"url": url} - ingress_data = {"ingress": ingress} - _validate_data(ingress_data, INGRESS_PROVIDES_APP_SCHEMA) - relation.data[self.app]["ingress"] = yaml.safe_dump(ingress) - - @property - def proxied_endpoints(self): - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - results[ingress_relation.app.name] = self._provided_url(ingress_relation) - - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url = None # type: str - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() - # used to prevent spur1ious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: str = None, - port: int = None, - strip_prefix: bool = False, - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - - self._stored.set_default(current_url=None) - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data(event.relation) - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: - self._stored.current_url = new_url - self.on.ready.emit(event.relation, new_url) - - def _handle_relation_broken(self, event): - self._stored.current_url = None - self.on.revoked.emit(event.relation) - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - for relation in self.relations: - self._publish_auto_data(relation) - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.warning("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self, relation: Relation): - if self._auto_data and self.unit.is_leader(): - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements(self, *, host: str = None, port: int): - """Publishes the data that Traefik needs to provide ingress. - - NB only the leader unit is supposed to do this. - - Args: - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - # get only the leader to publish the data since we only - # require one unit to publish it -- it will not differ between units, - # unlike in ingress-per-unit. - assert self.unit.is_leader(), "only leaders should do this." - assert self.relation, "no relation" - - if not host: - host = socket.getfqdn() - - data = { - "model": self.model.name, - "name": self.app.name, - "host": host, - "port": str(port), - } - - if self._strip_prefix: - data["strip-prefix"] = "true" - - _validate_data(data, INGRESS_REQUIRES_APP_SCHEMA) - self.relation.data[self.app].update(data) - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation: - return None - - # fetch the provider's app databag - try: - raw = relation.data.get(relation.app, {}).get("ingress") - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not raw: - return None - - ingress: ProviderIngressData = yaml.safe_load(raw) - _validate_data({"ingress": ingress}, INGRESS_PROVIDES_APP_SCHEMA) - return ingress["url"] - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = self._stored.current_url or None # type: ignore - assert isinstance(data, (str, type(None))) # for static checker - return data diff --git a/charms/openstack-hypervisor/osci.yaml b/charms/openstack-hypervisor/osci.yaml deleted file mode 100644 index 91848efd..00000000 --- a/charms/openstack-hypervisor/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: openstack-hypervisor - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/openstack-hypervisor/pyproject.toml b/charms/openstack-hypervisor/pyproject.toml deleted file mode 100644 index 30821404..00000000 --- a/charms/openstack-hypervisor/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/openstack-hypervisor/rename.sh b/charms/openstack-hypervisor/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/openstack-hypervisor/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/openstack-hypervisor/requirements.txt b/charms/openstack-hypervisor/requirements.txt index 17f89a30..978f759e 100644 --- a/charms/openstack-hypervisor/requirements.txt +++ b/charms/openstack-hypervisor/requirements.txt @@ -7,8 +7,9 @@ jinja2 cosl==0.0.5 ; python_version >= "3.8" pydantic==1.10.12 ; python_version >= "3.8" -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam - # This charm does not use lightkube* but ops_sunbeam requires it atm lightkube lightkube-models + +# From ops_sunbeam +tenacity diff --git a/charms/openstack-hypervisor/src/charm.py b/charms/openstack-hypervisor/src/charm.py index d77370a7..d8ad6b9f 100755 --- a/charms/openstack-hypervisor/src/charm.py +++ b/charms/openstack-hypervisor/src/charm.py @@ -51,7 +51,6 @@ from ops.charm import ( from ops.main import ( main, ) - from utils import ( get_local_ip_by_default_route, ) diff --git a/charms/openstack-hypervisor/test-requirements.txt b/charms/openstack-hypervisor/test-requirements.txt deleted file mode 100644 index 0982ac98..00000000 --- a/charms/openstack-hypervisor/test-requirements.txt +++ /dev/null @@ -1,15 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -coverage -mock -flake8 -stestr -ops -cosl==0.0.5 ; python_version >= "3.8" -pydantic==1.10.12 ; python_version >= "3.8" -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/openstack-hypervisor/tests/unit/test_charm.py b/charms/openstack-hypervisor/tests/unit/test_charm.py index 9b89b138..dbaba261 100644 --- a/charms/openstack-hypervisor/tests/unit/test_charm.py +++ b/charms/openstack-hypervisor/tests/unit/test_charm.py @@ -20,9 +20,8 @@ from unittest import ( mock, ) -import ops_sunbeam.test_utils as test_utils - import charm +import ops_sunbeam.test_utils as test_utils class _HypervisorOperatorCharm(charm.HypervisorOperatorCharm): diff --git a/charms/openstack-hypervisor/tox.ini b/charms/openstack-hypervisor/tox.ini deleted file mode 100644 index 38807328..00000000 --- a/charms/openstack-hypervisor/tox.ini +++ /dev/null @@ -1,168 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/ovn-central-k8s/.flake8 b/charms/ovn-central-k8s/.flake8 deleted file mode 100644 index 8ef84fcd..00000000 --- a/charms/ovn-central-k8s/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 99 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/charms/ovn-central-k8s/.gitignore b/charms/ovn-central-k8s/.gitignore deleted file mode 100644 index 73f116c9..00000000 --- a/charms/ovn-central-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -*.charm -*.swp - -.coverage -__pycache__/ -*.py[cod] -.tox -.stestr/ -tempest.log diff --git a/charms/ovn-central-k8s/.gitreview b/charms/ovn-central-k8s/.gitreview deleted file mode 100644 index 95d816b4..00000000 --- a/charms/ovn-central-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=x/charm-ovn-central-k8s.git -defaultbranch=main diff --git a/charms/ovn-central-k8s/.jujuignore b/charms/ovn-central-k8s/.jujuignore deleted file mode 100644 index 6ccd559e..00000000 --- a/charms/ovn-central-k8s/.jujuignore +++ /dev/null @@ -1,3 +0,0 @@ -/venv -*.py[cod] -*.charm diff --git a/charms/ovn-central-k8s/.stestr.conf b/charms/ovn-central-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/ovn-central-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/ovn-central-k8s/.zuul.yaml b/charms/ovn-central-k8s/.zuul.yaml deleted file mode 100644 index 0a41ba84..00000000 --- a/charms/ovn-central-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: ovn-central-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/ovn-central-k8s/charmcraft.yaml b/charms/ovn-central-k8s/charmcraft.yaml index eea1df04..e67d37b5 100644 --- a/charms/ovn-central-k8s/charmcraft.yaml +++ b/charms/ovn-central-k8s/charmcraft.yaml @@ -29,4 +29,3 @@ parts: - cryptography - jsonschema - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/ovn-central-k8s/fetch-libs.sh b/charms/ovn-central-k8s/fetch-libs.sh deleted file mode 100755 index 2b40c7b4..00000000 --- a/charms/ovn-central-k8s/fetch-libs.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates diff --git a/charms/ovn-central-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py b/charms/ovn-central-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py deleted file mode 100644 index 732679a6..00000000 --- a/charms/ovn-central-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py +++ /dev/null @@ -1,206 +0,0 @@ -"""TODO: Add a proper docstring here. - -This is a placeholder docstring for this charm library. Docstrings are -presented on Charmhub and updated whenever you push a new version of the -library. - -Complete documentation about creating and documenting libraries can be found -in the SDK docs at https://juju.is/docs/sdk/libraries. - -See `charmcraft publish-lib` and `charmcraft fetch-lib` for details of how to -share and consume charm libraries. They serve to enhance collaboration -between charmers. Use a charmer's libraries for classes that handle -integration with their charm. - -Bear in mind that new revisions of the different major API versions (v0, v1, -v2 etc) are maintained independently. You can continue to update v0 and v1 -after you have pushed v3. - -Markdown is supported, following the CommonMark specification. -""" - -import logging -import typing -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -# The unique Charmhub library identifier, never change it -LIBID = "114b7bb1970445daa61650e451f9da62" - -# Increment this major API version when introducing breaking changes -LIBAPI = 0 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 3 - - -# TODO: add your code here! Happy coding! -class OVSDBCMSConnectedEvent(EventBase): - """OVSDBCMS connected Event.""" - - pass - - -class OVSDBCMSReadyEvent(EventBase): - """OVSDBCMS ready for use Event.""" - - pass - - -class OVSDBCMSGoneAwayEvent(EventBase): - """OVSDBCMS relation has gone-away Event""" - - pass - - -class OVSDBCMSServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(OVSDBCMSConnectedEvent) - ready = EventSource(OVSDBCMSReadyEvent) - goneaway = EventSource(OVSDBCMSGoneAwayEvent) - - -class OVSDBCMSRequires(Object): - """ - OVSDBCMSRequires class - """ - - on = OVSDBCMSServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_ovsdb_cms_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_ovsdb_cms_relation_broken, - ) - - def _on_ovsdb_cms_relation_joined(self, event): - """OVSDBCMS relation joined.""" - logging.debug("OVSDBCMSRequires on_joined") - self.on.connected.emit() - - def bound_hostnames(self): - return self.get_all_unit_values("bound-hostname") - - def bound_addresses(self): - return self.get_all_unit_values("bound-address") - - def remote_ready(self): - return all(self.bound_hostnames()) or all(self.bound_addresses()) - - def _on_ovsdb_cms_relation_changed(self, event): - """OVSDBCMS relation changed.""" - logging.debug("OVSDBCMSRequires on_changed") - if self.remote_ready(): - self.on.ready.emit() - - def _on_ovsdb_cms_relation_broken(self, event): - """OVSDBCMS relation broken.""" - logging.debug("OVSDBCMSRequires on_broken") - self.on.goneaway.emit() - - def get_all_unit_values(self, key: str) -> typing.List[str]: - """Retrieve value for key from all related units.""" - values = [] - relation = self.framework.model.get_relation(self.relation_name) - if relation: - for unit in relation.units: - values.append(relation.data[unit].get(key)) - return values - - - -class OVSDBCMSClientConnectedEvent(EventBase): - """OVSDBCMS connected Event.""" - - pass - - -class OVSDBCMSClientReadyEvent(EventBase): - """OVSDBCMS ready for use Event.""" - - pass - - -class OVSDBCMSClientGoneAwayEvent(EventBase): - """OVSDBCMS relation has gone-away Event""" - - pass - - -class OVSDBCMSClientEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(OVSDBCMSClientConnectedEvent) - ready = EventSource(OVSDBCMSClientReadyEvent) - goneaway = EventSource(OVSDBCMSClientGoneAwayEvent) - - -class OVSDBCMSProvides(Object): - """ - OVSDBCMSProvides class - """ - - on = OVSDBCMSClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_ovsdb_cms_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_ovsdb_cms_relation_broken, - ) - - def _on_ovsdb_cms_relation_joined(self, event): - """Handle ovsdb-cms joined.""" - logging.debug("OVSDBCMSProvides on_joined") - self.on.connected.emit() - - def _on_ovsdb_cms_relation_changed(self, event): - """Handle ovsdb-cms changed.""" - logging.debug("OVSDBCMSProvides on_changed") - self.on.ready.emit() - - def _on_ovsdb_cms_relation_broken(self, event): - """Handle ovsdb-cms broken.""" - logging.debug("OVSDBCMSProvides on_departed") - self.on.goneaway.emit() - - def set_unit_data(self, settings: typing.Dict[str, str]) -> None: - """Publish settings on the peer unit data bag.""" - relations = self.framework.model.relations[self.relation_name] - for relation in relations: - for k, v in settings.items(): - relation.data[self.model.unit][k] = v diff --git a/charms/ovn-central-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py b/charms/ovn-central-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py deleted file mode 100644 index 1eda19bf..00000000 --- a/charms/ovn-central-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py +++ /dev/null @@ -1,1261 +0,0 @@ -# Copyright 2021 Canonical Ltd. -# See LICENSE file for licensing details. - -"""Library for the tls-certificates relation. - -This library contains the Requires and Provides classes for handling the tls-certificates -interface. - -## Getting Started -From a charm directory, fetch the library using `charmcraft`: - -```shell -charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates -``` - -Add the following libraries to the charm's `requirements.txt` file: -- jsonschema -- cryptography - -Add the following section to the charm's `charmcraft.yaml` file: -```yaml -parts: - charm: - build-packages: - - libffi-dev - - libssl-dev - - rustc - - cargo -``` - -### Provider charm -The provider charm is the charm providing certificates to another charm that requires them. In -this example, the provider charm is storing its private key using a peer relation interface called -`replicas`. - -Example: -```python -from charms.tls_certificates_interface.v1.tls_certificates import ( - CertificateCreationRequestEvent, - CertificateRevocationRequestEvent, - TLSCertificatesProvidesV1, - generate_private_key, -) -from ops.charm import CharmBase, InstallEvent -from ops.main import main -from ops.model import ActiveStatus, WaitingStatus - - -def generate_ca(private_key: bytes, subject: str) -> str: - return "whatever ca content" - - -def generate_certificate(ca: str, private_key: str, csr: str) -> str: - return "Whatever certificate" - - -class ExampleProviderCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - self.certificates = TLSCertificatesProvidesV1(self, "certificates") - self.framework.observe( - self.certificates.on.certificate_request, self._on_certificate_request - ) - self.framework.observe( - self.certificates.on.certificate_revoked, self._on_certificate_revocation_request - ) - self.framework.observe(self.on.install, self._on_install) - - def _on_install(self, event: InstallEvent) -> None: - private_key_password = b"banana" - private_key = generate_private_key(password=private_key_password) - ca_certificate = generate_ca(private_key=private_key, subject="whatever") - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update( - { - "private_key_password": "banana", - "private_key": private_key, - "ca_certificate": ca_certificate, - } - ) - self.unit.status = ActiveStatus() - - def _on_certificate_request(self, event: CertificateCreationRequestEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - ca_certificate = replicas_relation.data[self.app].get("ca_certificate") - private_key = replicas_relation.data[self.app].get("private_key") - certificate = generate_certificate( - ca=ca_certificate, - private_key=private_key, - csr=event.certificate_signing_request, - ) - - self.certificates.set_relation_certificate( - certificate=certificate, - certificate_signing_request=event.certificate_signing_request, - ca=ca_certificate, - chain=[ca_certificate, certificate], - relation_id=event.relation_id, - ) - - def _on_certificate_revocation_request(self, event: CertificateRevocationRequestEvent) -> None: - # Do what you want to do with this information - pass - - -if __name__ == "__main__": - main(ExampleProviderCharm) -``` - -### Requirer charm -The requirer charm is the charm requiring certificates from another charm that provides them. In -this example, the requirer charm is storing its certificates using a peer relation interface called -`replicas`. - -Example: -```python -from charms.tls_certificates_interface.v1.tls_certificates import ( - CertificateAvailableEvent, - CertificateExpiringEvent, - TLSCertificatesRequiresV1, - generate_csr, - generate_private_key, -) -from ops.charm import CharmBase, RelationJoinedEvent -from ops.main import main -from ops.model import ActiveStatus, WaitingStatus - - -class ExampleRequirerCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - self.cert_subject = "whatever" - self.certificates = TLSCertificatesRequiresV1(self, "certificates") - self.framework.observe(self.on.install, self._on_install) - self.framework.observe( - self.on.certificates_relation_joined, self._on_certificates_relation_joined - ) - self.framework.observe( - self.certificates.on.certificate_available, self._on_certificate_available - ) - self.framework.observe( - self.certificates.on.certificate_expiring, self._on_certificate_expiring - ) - - def _on_install(self, event) -> None: - private_key_password = b"banana" - private_key = generate_private_key(password=private_key_password) - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update( - {"private_key_password": "banana", "private_key": private_key.decode()} - ) - - def _on_certificates_relation_joined(self, event: RelationJoinedEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - replicas_relation.data[self.app].update({"csr": csr.decode()}) - self.certificates.request_certificate_creation(certificate_signing_request=csr) - - def _on_certificate_available(self, event: CertificateAvailableEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update({"certificate": event.certificate}) - replicas_relation.data[self.app].update({"ca": event.ca}) - replicas_relation.data[self.app].update({"chain": event.chain}) - self.unit.status = ActiveStatus() - - def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - old_csr = replicas_relation.data[self.app].get("csr") - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - new_csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - self.certificates.request_certificate_renewal( - old_certificate_signing_request=old_csr, - new_certificate_signing_request=new_csr, - ) - replicas_relation.data[self.app].update({"csr": new_csr.decode()}) - - -if __name__ == "__main__": - main(ExampleRequirerCharm) -``` -""" # noqa: D405, D410, D411, D214, D416 - -import copy -import json -import logging -import uuid -from datetime import datetime, timedelta -from ipaddress import IPv4Address -from typing import Dict, List, Optional - -from cryptography import x509 -from cryptography.hazmat._oid import ExtensionOID -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.hazmat.primitives.serialization import pkcs12 -from cryptography.x509.extensions import Extension, ExtensionNotFound -from jsonschema import exceptions, validate # type: ignore[import] -from ops.charm import CharmBase, CharmEvents, RelationChangedEvent, UpdateStatusEvent -from ops.framework import EventBase, EventSource, Handle, Object - -# The unique Charmhub library identifier, never change it -LIBID = "afd8c2bccf834997afce12c2706d2ede" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 10 - -REQUIRER_JSON_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/requirer.json", # noqa: E501 - "type": "object", - "title": "`tls_certificates` requirer root schema", - "description": "The `tls_certificates` root schema comprises the entire requirer databag for this interface.", # noqa: E501 - "examples": [ - { - "certificate_signing_requests": [ - { - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\\n-----END CERTIFICATE REQUEST-----\\n" # noqa: E501 - }, - { - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBAMk3raaX803cHvzlBF9LC7KORT46z4VjyU5PIaMb\\nQLIDgYKFYI0n5hf2Ra4FAHvOvEmW7bjNlHORFEmvnpcU5kPMNUyKFMTaC8LGmN8z\\nUBH3aK+0+FRvY4afn9tgj5435WqOG9QdoDJ0TJkjJbJI9M70UOgL711oU7ql6HxU\\n4d2ydFK9xAHrBwziNHgNZ72L95s4gLTXf0fAHYf15mDA9U5yc+YDubCKgTXzVySQ\\nUx73VCJLfC/XkZIh559IrnRv5G9fu6BMLEuBwAz6QAO4+/XidbKWN4r2XSq5qX4n\\n6EPQQWP8/nd4myq1kbg6Q8w68L/0YdfjCmbyf2TuoWeImdUCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQBIdwraBvpYo/rl5MH1+1Um6HRg4gOdQPY5WcJy9B9tgzJz\\nittRSlRGTnhyIo6fHgq9KHrmUthNe8mMTDailKFeaqkVNVvk7l0d1/B90Kz6OfmD\\nxN0qjW53oP7y3QB5FFBM8DjqjmUnz5UePKoX4AKkDyrKWxMwGX5RoET8c/y0y9jp\\nvSq3Wh5UpaZdWbe1oVY8CqMVUEVQL2DPjtopxXFz2qACwsXkQZxWmjvZnRiP8nP8\\nbdFaEuh9Q6rZ2QdZDEtrU4AodPU3NaukFr5KlTUQt3w/cl+5//zils6G5zUWJ2pN\\ng7+t9PTvXHRkH+LnwaVnmsBFU2e05qADQbfIn7JA\\n-----END CERTIFICATE REQUEST-----\\n" # noqa: E501 - }, - ] - } - ], - "properties": { - "certificate_signing_requests": { - "type": "array", - "items": { - "type": "object", - "properties": {"certificate_signing_request": {"type": "string"}}, - "required": ["certificate_signing_request"], - }, - } - }, - "required": ["certificate_signing_requests"], - "additionalProperties": True, -} - -PROVIDER_JSON_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/provider.json", # noqa: E501 - "type": "object", - "title": "`tls_certificates` provider root schema", - "description": "The `tls_certificates` root schema comprises the entire provider databag for this interface.", # noqa: E501 - "example": [ - { - "certificates": [ - { - "ca": "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n", # noqa: E501 - "chain": [ - "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n" # noqa: E501, W505 - ], - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\n-----END CERTIFICATE REQUEST-----\n", # noqa: E501 - "certificate": "-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n", # noqa: E501 - } - ] - } - ], - "properties": { - "certificates": { - "$id": "#/properties/certificates", - "type": "array", - "items": { - "$id": "#/properties/certificates/items", - "type": "object", - "required": ["certificate_signing_request", "certificate", "ca", "chain"], - "properties": { - "certificate_signing_request": { - "$id": "#/properties/certificates/items/certificate_signing_request", - "type": "string", - }, - "certificate": { - "$id": "#/properties/certificates/items/certificate", - "type": "string", - }, - "ca": {"$id": "#/properties/certificates/items/ca", "type": "string"}, - "chain": { - "$id": "#/properties/certificates/items/chain", - "type": "array", - "items": { - "type": "string", - "$id": "#/properties/certificates/items/chain/items", - }, - }, - }, - "additionalProperties": True, - }, - } - }, - "required": ["certificates"], - "additionalProperties": True, -} - - -logger = logging.getLogger(__name__) - - -class CertificateAvailableEvent(EventBase): - """Charm Event triggered when a TLS certificate is available.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - - -class CertificateExpiringEvent(EventBase): - """Charm Event triggered when a TLS certificate is almost expired.""" - - def __init__(self, handle, certificate: str, expiry: str): - """CertificateExpiringEvent. - - Args: - handle (Handle): Juju framework handle - certificate (str): TLS Certificate - expiry (str): Datetime string reprensenting the time at which the certificate - won't be valid anymore. - """ - super().__init__(handle) - self.certificate = certificate - self.expiry = expiry - - def snapshot(self) -> dict: - """Returns snapshot.""" - return {"certificate": self.certificate, "expiry": self.expiry} - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.expiry = snapshot["expiry"] - - -class CertificateExpiredEvent(EventBase): - """Charm Event triggered when a TLS certificate is expired.""" - - def __init__(self, handle: Handle, certificate: str): - super().__init__(handle) - self.certificate = certificate - - def snapshot(self) -> dict: - """Returns snapshot.""" - return {"certificate": self.certificate} - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - - -class CertificateCreationRequestEvent(EventBase): - """Charm Event triggered when a TLS certificate is required.""" - - def __init__(self, handle: Handle, certificate_signing_request: str, relation_id: int): - super().__init__(handle) - self.certificate_signing_request = certificate_signing_request - self.relation_id = relation_id - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate_signing_request": self.certificate_signing_request, - "relation_id": self.relation_id, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.relation_id = snapshot["relation_id"] - - -class CertificateRevocationRequestEvent(EventBase): - """Charm Event triggered when a TLS certificate needs to be revoked.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: str, - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - - -def _load_relation_data(raw_relation_data: dict) -> dict: - """Loads relation data from the relation data bag. - - Json loads all data. - - Args: - raw_relation_data: Relation data from the databag - - Returns: - dict: Relation data in dict format. - """ - certificate_data = dict() - for key in raw_relation_data: - try: - certificate_data[key] = json.loads(raw_relation_data[key]) - except (json.decoder.JSONDecodeError, TypeError): - certificate_data[key] = raw_relation_data[key] - return certificate_data - - -def generate_ca( - private_key: bytes, - subject: str, - private_key_password: Optional[bytes] = None, - validity: int = 365, - country: str = "US", -) -> bytes: - """Generates a CA Certificate. - - Args: - private_key (bytes): Private key - subject (str): Certificate subject - private_key_password (bytes): Private key password - validity (int): Certificate validity time (in days) - country (str): Certificate Issuing country - - Returns: - bytes: CA Certificate. - """ - private_key_object = serialization.load_pem_private_key( - private_key, password=private_key_password - ) - subject = issuer = x509.Name( - [ - x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country), - x509.NameAttribute(x509.NameOID.COMMON_NAME, subject), - ] - ) - subject_identifier_object = x509.SubjectKeyIdentifier.from_public_key( - private_key_object.public_key() # type: ignore[arg-type] - ) - subject_identifier = key_identifier = subject_identifier_object.public_bytes() - cert = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(private_key_object.public_key()) # type: ignore[arg-type] - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.utcnow()) - .not_valid_after(datetime.utcnow() + timedelta(days=validity)) - .add_extension(x509.SubjectKeyIdentifier(digest=subject_identifier), critical=False) - .add_extension( - x509.AuthorityKeyIdentifier( - key_identifier=key_identifier, - authority_cert_issuer=None, - authority_cert_serial_number=None, - ), - critical=False, - ) - .add_extension( - x509.BasicConstraints(ca=True, path_length=None), - critical=True, - ) - .sign(private_key_object, hashes.SHA256()) # type: ignore[arg-type] - ) - return cert.public_bytes(serialization.Encoding.PEM) - - -def generate_certificate( - csr: bytes, - ca: bytes, - ca_key: bytes, - ca_key_password: Optional[bytes] = None, - validity: int = 365, - alt_names: List[str] = None, -) -> bytes: - """Generates a TLS certificate based on a CSR. - - Args: - csr (bytes): CSR - ca (bytes): CA Certificate - ca_key (bytes): CA private key - ca_key_password: CA private key password - validity (int): Certificate validity (in days) - alt_names (list): List of alt names to put on cert - prefer putting SANs in CSR - - Returns: - bytes: Certificate - """ - csr_object = x509.load_pem_x509_csr(csr) - subject = csr_object.subject - issuer = x509.load_pem_x509_certificate(ca).issuer - private_key = serialization.load_pem_private_key(ca_key, password=ca_key_password) - - certificate_builder = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(csr_object.public_key()) - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.utcnow()) - .not_valid_after(datetime.utcnow() + timedelta(days=validity)) - ) - - extensions_list = csr_object.extensions - san_ext: Optional[x509.Extension] = None - if alt_names: - full_sans_dns = alt_names.copy() - try: - loaded_san_ext = csr_object.extensions.get_extension_for_class( - x509.SubjectAlternativeName - ) - full_sans_dns.extend(loaded_san_ext.value.get_values_for_type(x509.DNSName)) - except ExtensionNotFound: - pass - finally: - san_ext = Extension( - ExtensionOID.SUBJECT_ALTERNATIVE_NAME, - False, - x509.SubjectAlternativeName([x509.DNSName(name) for name in full_sans_dns]), - ) - if not extensions_list: - extensions_list = x509.Extensions([san_ext]) - - for extension in extensions_list: - if extension.value.oid == ExtensionOID.SUBJECT_ALTERNATIVE_NAME and san_ext: - extension = san_ext - - certificate_builder = certificate_builder.add_extension( - extension.value, - critical=extension.critical, - ) - certificate_builder._version = x509.Version.v3 - cert = certificate_builder.sign(private_key, hashes.SHA256()) # type: ignore[arg-type] - return cert.public_bytes(serialization.Encoding.PEM) - - -def generate_pfx_package( - certificate: bytes, - private_key: bytes, - package_password: str, - private_key_password: Optional[bytes] = None, -) -> bytes: - """Generates a PFX package to contain the TLS certificate and private key. - - Args: - certificate (bytes): TLS certificate - private_key (bytes): Private key - package_password (str): Password to open the PFX package - private_key_password (bytes): Private key password - - Returns: - bytes: - """ - private_key_object = serialization.load_pem_private_key( - private_key, password=private_key_password - ) - certificate_object = x509.load_pem_x509_certificate(certificate) - name = certificate_object.subject.rfc4514_string() - pfx_bytes = pkcs12.serialize_key_and_certificates( - name=name.encode(), - cert=certificate_object, - key=private_key_object, # type: ignore[arg-type] - cas=None, - encryption_algorithm=serialization.BestAvailableEncryption(package_password.encode()), - ) - return pfx_bytes - - -def generate_private_key( - password: Optional[bytes] = None, - key_size: int = 2048, - public_exponent: int = 65537, -) -> bytes: - """Generates a private key. - - Args: - password (bytes): Password for decrypting the private key - key_size (int): Key size in bytes - public_exponent: Public exponent. - - Returns: - bytes: Private Key - """ - private_key = rsa.generate_private_key( - public_exponent=public_exponent, - key_size=key_size, - ) - key_bytes = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.BestAvailableEncryption(password) - if password - else serialization.NoEncryption(), - ) - return key_bytes - - -def generate_csr( - private_key: bytes, - subject: str, - add_unique_id_to_subject_name: bool = True, - organization: str = None, - email_address: str = None, - country_name: str = None, - private_key_password: Optional[bytes] = None, - sans: Optional[List[str]] = None, - sans_oid: Optional[List[str]] = None, - sans_ip: Optional[List[str]] = None, - sans_dns: Optional[List[str]] = None, - additional_critical_extensions: Optional[List] = None, -) -> bytes: - """Generates a CSR using private key and subject. - - Args: - private_key (bytes): Private key - subject (str): CSR Subject. - add_unique_id_to_subject_name (bool): Whether a unique ID must be added to the CSR's - subject name. Always leave to "True" when the CSR is used to request certificates - using the tls-certificates relation. - organization (str): Name of organization. - email_address (str): Email address. - country_name (str): Country Name. - private_key_password (bytes): Private key password - sans (list): Use sans_dns - this will be deprecated in a future release - List of DNS subject alternative names (keeping it for now for backward compatibility) - sans_oid (list): List of registered ID SANs - sans_dns (list): List of DNS subject alternative names (similar to the arg: sans) - sans_ip (list): List of IP subject alternative names - additional_critical_extensions (list): List if critical additional extension objects. - Object must be a x509 ExtensionType. - - Returns: - bytes: CSR - """ - signing_key = serialization.load_pem_private_key(private_key, password=private_key_password) - subject_name = [x509.NameAttribute(x509.NameOID.COMMON_NAME, subject)] - if add_unique_id_to_subject_name: - unique_identifier = uuid.uuid4() - subject_name.append( - x509.NameAttribute(x509.NameOID.X500_UNIQUE_IDENTIFIER, str(unique_identifier)) - ) - if organization: - subject_name.append(x509.NameAttribute(x509.NameOID.ORGANIZATION_NAME, organization)) - if email_address: - subject_name.append(x509.NameAttribute(x509.NameOID.EMAIL_ADDRESS, email_address)) - if country_name: - subject_name.append(x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country_name)) - csr = x509.CertificateSigningRequestBuilder(subject_name=x509.Name(subject_name)) - - _sans: List[x509.GeneralName] = [] - if sans_oid: - _sans.extend([x509.RegisteredID(x509.ObjectIdentifier(san)) for san in sans_oid]) - if sans_ip: - _sans.extend([x509.IPAddress(IPv4Address(san)) for san in sans_ip]) - if sans: - _sans.extend([x509.DNSName(san) for san in sans]) - if sans_dns: - _sans.extend([x509.DNSName(san) for san in sans_dns]) - if _sans: - csr = csr.add_extension(x509.SubjectAlternativeName(set(_sans)), critical=False) - - if additional_critical_extensions: - for extension in additional_critical_extensions: - csr = csr.add_extension(extension, critical=True) - - signed_certificate = csr.sign(signing_key, hashes.SHA256()) # type: ignore[arg-type] - return signed_certificate.public_bytes(serialization.Encoding.PEM) - - -class CertificatesProviderCharmEvents(CharmEvents): - """List of events that the TLS Certificates provider charm can leverage.""" - - certificate_creation_request = EventSource(CertificateCreationRequestEvent) - certificate_revocation_request = EventSource(CertificateRevocationRequestEvent) - - -class CertificatesRequirerCharmEvents(CharmEvents): - """List of events that the TLS Certificates requirer charm can leverage.""" - - certificate_available = EventSource(CertificateAvailableEvent) - certificate_expiring = EventSource(CertificateExpiringEvent) - certificate_expired = EventSource(CertificateExpiredEvent) - - -class TLSCertificatesProvidesV1(Object): - """TLS certificates provider class to be instantiated by TLS certificates providers.""" - - on = CertificatesProviderCharmEvents() - - def __init__(self, charm: CharmBase, relationship_name: str): - super().__init__(charm, relationship_name) - self.framework.observe( - charm.on[relationship_name].relation_changed, self._on_relation_changed - ) - self.charm = charm - self.relationship_name = relationship_name - - def _add_certificate( - self, - relation_id: int, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - ) -> None: - """Adds certificate to relation data. - - Args: - relation_id (int): Relation id - certificate (str): Certificate - certificate_signing_request (str): Certificate Signing Request - ca (str): CA Certificate - chain (list): CA Chain - - Returns: - None - """ - relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - new_certificate = { - "certificate": certificate, - "certificate_signing_request": certificate_signing_request, - "ca": ca, - "chain": chain, - } - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = provider_relation_data.get("certificates", []) - certificates = copy.deepcopy(provider_certificates) - if new_certificate in certificates: - logger.info("Certificate already in relation data - Doing nothing") - return - certificates.append(new_certificate) - relation.data[self.model.app]["certificates"] = json.dumps(certificates) - - def _remove_certificate( - self, - relation_id: int, - certificate: str = None, - certificate_signing_request: str = None, - ) -> None: - """Removes certificate from a given relation based on user provided certificate or csr. - - Args: - relation_id (int): Relation id - certificate (str): Certificate (optional) - certificate_signing_request: Certificate signing request (optional) - - Returns: - None - """ - relation = self.model.get_relation( - relation_name=self.relationship_name, - relation_id=relation_id, - ) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} with relation id {relation_id} does not exist" - ) - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = provider_relation_data.get("certificates", []) - certificates = copy.deepcopy(provider_certificates) - for certificate_dict in certificates: - if certificate and certificate_dict["certificate"] == certificate: - certificates.remove(certificate_dict) - if ( - certificate_signing_request - and certificate_dict["certificate_signing_request"] == certificate_signing_request - ): - certificates.remove(certificate_dict) - relation.data[self.model.app]["certificates"] = json.dumps(certificates) - - @staticmethod - def _relation_data_is_valid(certificates_data: dict) -> bool: - """Uses JSON schema validator to validate relation data content. - - Args: - certificates_data (dict): Certificate data dictionary as retrieved from relation data. - - Returns: - bool: True/False depending on whether the relation data follows the json schema. - """ - try: - validate(instance=certificates_data, schema=REQUIRER_JSON_SCHEMA) - return True - except exceptions.ValidationError: - return False - - def revoke_all_certificates(self) -> None: - """Revokes all certificates of this provider. - - This method is meant to be used when the Root CA has changed. - """ - for relation in self.model.relations[self.relationship_name]: - relation.data[self.model.app]["certificates"] = json.dumps([]) - - def set_relation_certificate( - self, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - relation_id: int, - ) -> None: - """Adds certificates to relation data. - - Args: - certificate (str): Certificate - certificate_signing_request (str): Certificate signing request - ca (str): CA Certificate - chain (list): CA Chain - relation_id (int): Juju relation ID - - Returns: - None - """ - certificates_relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - self._remove_certificate( - certificate_signing_request=certificate_signing_request.strip(), - relation_id=relation_id, - ) - self._add_certificate( - relation_id=relation_id, - certificate=certificate.strip(), - certificate_signing_request=certificate_signing_request.strip(), - ca=ca.strip(), - chain=[cert.strip() for cert in chain], - ) - - def remove_certificate(self, certificate: str) -> None: - """Removes a given certificate from relation data. - - Args: - certificate (str): TLS Certificate - - Returns: - None - """ - certificates_relation = self.model.relations[self.relationship_name] - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - for certificate_relation in certificates_relation: - self._remove_certificate(certificate=certificate, relation_id=certificate_relation.id) - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handler triggerred on relation changed event. - - Looks at the relation data and either emits: - - certificate request event: If the unit relation data contains a CSR for which - a certificate does not exist in the provider relation data. - - certificate revocation event: If the provider relation data contains a CSR for which - a csr does not exist in the requirer relation data. - - Args: - event: Juju event - - Returns: - None - """ - assert event.unit is not None - requirer_relation_data = _load_relation_data(event.relation.data[event.unit]) - provider_relation_data = _load_relation_data(event.relation.data[self.charm.app]) - if not self._relation_data_is_valid(requirer_relation_data): - logger.warning( - f"Relation data did not pass JSON Schema validation: {requirer_relation_data}" - ) - return - provider_certificates = provider_relation_data.get("certificates", []) - requirer_csrs = requirer_relation_data.get("certificate_signing_requests", []) - provider_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in provider_certificates - ] - requirer_unit_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in requirer_csrs - ] - for certificate_signing_request in requirer_unit_csrs: - if certificate_signing_request not in provider_csrs: - self.on.certificate_creation_request.emit( - certificate_signing_request=certificate_signing_request, - relation_id=event.relation.id, - ) - self._revoke_certificates_for_which_no_csr_exists(relation_id=event.relation.id) - - def _revoke_certificates_for_which_no_csr_exists(self, relation_id: int) -> None: - """Revokes certificates for which no unit has a CSR. - - Goes through all generated certificates and compare agains the list of CSRS for all units - of a given relationship. - - Args: - relation_id (int): Relation id - - Returns: - None - """ - certificates_relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - provider_relation_data = _load_relation_data(certificates_relation.data[self.charm.app]) - list_of_csrs: List[str] = [] - for unit in certificates_relation.units: - requirer_relation_data = _load_relation_data(certificates_relation.data[unit]) - requirer_csrs = requirer_relation_data.get("certificate_signing_requests", []) - list_of_csrs.extend(csr["certificate_signing_request"] for csr in requirer_csrs) - provider_certificates = provider_relation_data.get("certificates", []) - for certificate in provider_certificates: - if certificate["certificate_signing_request"] not in list_of_csrs: - self.on.certificate_revocation_request.emit( - certificate=certificate["certificate"], - certificate_signing_request=certificate["certificate_signing_request"], - ca=certificate["ca"], - chain=certificate["chain"], - ) - self.remove_certificate(certificate=certificate["certificate"]) - - -class TLSCertificatesRequiresV1(Object): - """TLS certificates requirer class to be instantiated by TLS certificates requirers.""" - - on = CertificatesRequirerCharmEvents() - - def __init__( - self, - charm: CharmBase, - relationship_name: str, - expiry_notification_time: int = 168, - ): - """Generates/use private key and observes relation changed event. - - Args: - charm: Charm object - relationship_name: Juju relation name - expiry_notification_time (int): Time difference between now and expiry (in hours). - Used to trigger the CertificateExpiring event. Default: 7 days. - """ - super().__init__(charm, relationship_name) - self.relationship_name = relationship_name - self.charm = charm - self.expiry_notification_time = expiry_notification_time - self.framework.observe( - charm.on[relationship_name].relation_changed, self._on_relation_changed - ) - self.framework.observe(charm.on.update_status, self._on_update_status) - - @property - def _requirer_csrs(self) -> List[Dict[str, str]]: - """Returns list of requirer CSR's from relation data.""" - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - requirer_relation_data = _load_relation_data(relation.data[self.model.unit]) - return requirer_relation_data.get("certificate_signing_requests", []) - - @property - def _provider_certificates(self) -> List[Dict[str, str]]: - """Returns list of provider CSR's from relation data.""" - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - if not relation.app: - raise RuntimeError(f"Remote app for relation {self.relationship_name} does not exist") - provider_relation_data = _load_relation_data(relation.data[relation.app]) - return provider_relation_data.get("certificates", []) - - def _add_requirer_csr(self, csr: str) -> None: - """Adds CSR to relation data. - - Args: - csr (str): Certificate Signing Request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - new_csr_dict = {"certificate_signing_request": csr} - if new_csr_dict in self._requirer_csrs: - logger.info("CSR already in relation data - Doing nothing") - return - requirer_csrs = copy.deepcopy(self._requirer_csrs) - requirer_csrs.append(new_csr_dict) - relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs) - - def _remove_requirer_csr(self, csr: str) -> None: - """Removes CSR from relation data. - - Args: - csr (str): Certificate signing request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - requirer_csrs = copy.deepcopy(self._requirer_csrs) - csr_dict = {"certificate_signing_request": csr} - if csr_dict not in requirer_csrs: - logger.info("CSR not in relation data - Doing nothing") - return - requirer_csrs.remove(csr_dict) - relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs) - - def request_certificate_creation(self, certificate_signing_request: bytes) -> None: - """Request TLS certificate to provider charm. - - Args: - certificate_signing_request (bytes): Certificate Signing Request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - message = ( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - logger.error(message) - raise RuntimeError(message) - self._add_requirer_csr(certificate_signing_request.decode().strip()) - logger.info("Certificate request sent to provider") - - def request_certificate_revocation(self, certificate_signing_request: bytes) -> None: - """Removes CSR from relation data. - - The provider of this relation is then expected to remove certificates associated to this - CSR from the relation data as well and emit a request_certificate_revocation event for the - provider charm to interpret. - - Args: - certificate_signing_request (bytes): Certificate Signing Request - - Returns: - None - """ - self._remove_requirer_csr(certificate_signing_request.decode().strip()) - logger.info("Certificate revocation sent to provider") - - def request_certificate_renewal( - self, old_certificate_signing_request: bytes, new_certificate_signing_request: bytes - ) -> None: - """Renews certificate. - - Removes old CSR from relation data and adds new one. - - Args: - old_certificate_signing_request: Old CSR - new_certificate_signing_request: New CSR - - Returns: - None - """ - try: - self.request_certificate_revocation( - certificate_signing_request=old_certificate_signing_request - ) - except RuntimeError: - logger.warning("Certificate revocation failed.") - self.request_certificate_creation( - certificate_signing_request=new_certificate_signing_request - ) - logger.info("Certificate renewal request completed.") - - @staticmethod - def _relation_data_is_valid(certificates_data: dict) -> bool: - """Checks whether relation data is valid based on json schema. - - Args: - certificates_data: Certificate data in dict format. - - Returns: - bool: Whether relation data is valid. - """ - try: - validate(instance=certificates_data, schema=PROVIDER_JSON_SCHEMA) - return True - except exceptions.ValidationError: - return False - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handler triggerred on relation changed events. - - Args: - event: Juju event - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - logger.warning(f"No relation: {self.relationship_name}") - return - if not relation.app: - logger.warning(f"No remote app in relation: {self.relationship_name}") - return - provider_relation_data = _load_relation_data(relation.data[relation.app]) - if not self._relation_data_is_valid(provider_relation_data): - logger.warning( - f"Provider relation data did not pass JSON Schema validation: " - f"{event.relation.data[relation.app]}" - ) - return - requirer_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in self._requirer_csrs - ] - for certificate in self._provider_certificates: - if certificate["certificate_signing_request"] in requirer_csrs: - self.on.certificate_available.emit( - certificate_signing_request=certificate["certificate_signing_request"], - certificate=certificate["certificate"], - ca=certificate["ca"], - chain=certificate["chain"], - ) - - def _on_update_status(self, event: UpdateStatusEvent) -> None: - """Triggered on update status event. - - Goes through each certificate in the "certificates" relation and checks their expiry date. - If they are close to expire (<7 days), emits a CertificateExpiringEvent event and if - they are expired, emits a CertificateExpiredEvent. - - Args: - event (UpdateStatusEvent): Juju event - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - logger.warning(f"No relation: {self.relationship_name}") - return - if not relation.app: - logger.warning(f"No remote app in relation: {self.relationship_name}") - return - provider_relation_data = _load_relation_data(relation.data[relation.app]) - if not self._relation_data_is_valid(provider_relation_data): - logger.warning( - f"Provider relation data did not pass JSON Schema validation: " - f"{relation.data[relation.app]}" - ) - return - for certificate_dict in self._provider_certificates: - certificate = certificate_dict["certificate"] - try: - certificate_object = x509.load_pem_x509_certificate(data=certificate.encode()) - except ValueError: - logger.warning("Could not load certificate.") - continue - time_difference = certificate_object.not_valid_after - datetime.utcnow() - if time_difference.total_seconds() < 0: - logger.warning("Certificate is expired") - self.on.certificate_expired.emit(certificate=certificate) - self.request_certificate_revocation(certificate.encode()) - continue - if time_difference.total_seconds() < (self.expiry_notification_time * 60 * 60): - logger.warning("Certificate almost expired") - self.on.certificate_expiring.emit( - certificate=certificate, expiry=certificate_object.not_valid_after.isoformat() - ) diff --git a/charms/ovn-central-k8s/osci.yaml b/charms/ovn-central-k8s/osci.yaml deleted file mode 100644 index 15e9a479..00000000 --- a/charms/ovn-central-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: ovn-central-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 23.09/edge diff --git a/charms/ovn-central-k8s/pyproject.toml b/charms/ovn-central-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/ovn-central-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/ovn-central-k8s/rename.sh b/charms/ovn-central-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/ovn-central-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/ovn-central-k8s/requirements.txt b/charms/ovn-central-k8s/requirements.txt index 9211d667..ddbedec1 100644 --- a/charms/ovn-central-k8s/requirements.txt +++ b/charms/ovn-central-k8s/requirements.txt @@ -11,4 +11,5 @@ lightkube lightkube-models ops -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam +# From ops_sunbeam +tenacity diff --git a/charms/ovn-central-k8s/src/charm.py b/charms/ovn-central-k8s/src/charm.py index ad0971ee..7158c4a7 100755 --- a/charms/ovn-central-k8s/src/charm.py +++ b/charms/ovn-central-k8s/src/charm.py @@ -35,6 +35,8 @@ import ops_sunbeam.ovn.config_contexts as ovn_ctxts import ops_sunbeam.ovn.container_handlers as ovn_chandlers import ops_sunbeam.ovn.relation_handlers as ovn_rhandlers import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ovn +import ovsdb as ch_ovsdb import tenacity from ops.framework import ( StoredState, @@ -43,9 +45,6 @@ from ops.main import ( main, ) -import ovn -import ovsdb as ch_ovsdb - logger = logging.getLogger(__name__) OVN_SB_DB_CONTAINER = "ovn-sb-db-server" diff --git a/charms/ovn-central-k8s/test-requirements.txt b/charms/ovn-central-k8s/test-requirements.txt deleted file mode 100644 index da3b04b7..00000000 --- a/charms/ovn-central-k8s/test-requirements.txt +++ /dev/null @@ -1,12 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -coverage -mock -stestr -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/ovn-central-k8s/tests/unit/test_ovn_central_charm.py b/charms/ovn-central-k8s/tests/unit/test_ovn_central_charm.py index 2b4e5c08..ab676826 100644 --- a/charms/ovn-central-k8s/tests/unit/test_ovn_central_charm.py +++ b/charms/ovn-central-k8s/tests/unit/test_ovn_central_charm.py @@ -16,11 +16,10 @@ """Tests for OVN central charm.""" +import charm import mock import ops_sunbeam.test_utils as test_utils -import charm - class _OVNCentralOperatorCharm(charm.OVNCentralOperatorCharm): def __init__(self, framework): diff --git a/charms/ovn-central-k8s/tox.ini b/charms/ovn-central-k8s/tox.ini deleted file mode 100644 index 2b8f98b1..00000000 --- a/charms/ovn-central-k8s/tox.ini +++ /dev/null @@ -1,169 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - PYTHONPATH - HOME -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/ovn-relay-k8s/.flake8 b/charms/ovn-relay-k8s/.flake8 deleted file mode 100644 index 8ef84fcd..00000000 --- a/charms/ovn-relay-k8s/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 99 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/charms/ovn-relay-k8s/.gitignore b/charms/ovn-relay-k8s/.gitignore deleted file mode 100644 index de9170b0..00000000 --- a/charms/ovn-relay-k8s/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -venv/ -build/ -*.charm -*.swp - -.coverage -__pycache__/ -*.py[cod] -.tox -.stestr/ diff --git a/charms/ovn-relay-k8s/.gitreview b/charms/ovn-relay-k8s/.gitreview deleted file mode 100644 index 5f989225..00000000 --- a/charms/ovn-relay-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=x/charm-ovn-relay-k8s.git -defaultbranch=main diff --git a/charms/ovn-relay-k8s/.jujuignore b/charms/ovn-relay-k8s/.jujuignore deleted file mode 100644 index 6ccd559e..00000000 --- a/charms/ovn-relay-k8s/.jujuignore +++ /dev/null @@ -1,3 +0,0 @@ -/venv -*.py[cod] -*.charm diff --git a/charms/ovn-relay-k8s/.stestr.conf b/charms/ovn-relay-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/ovn-relay-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/ovn-relay-k8s/.zuul.yaml b/charms/ovn-relay-k8s/.zuul.yaml deleted file mode 100644 index 494e48c6..00000000 --- a/charms/ovn-relay-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-yoga-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: ovn-relay-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/ovn-relay-k8s/charmcraft.yaml b/charms/ovn-relay-k8s/charmcraft.yaml index eea1df04..e67d37b5 100644 --- a/charms/ovn-relay-k8s/charmcraft.yaml +++ b/charms/ovn-relay-k8s/charmcraft.yaml @@ -29,4 +29,3 @@ parts: - cryptography - jsonschema - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/ovn-relay-k8s/fetch-libs.sh b/charms/ovn-relay-k8s/fetch-libs.sh deleted file mode 100755 index bbf34c7b..00000000 --- a/charms/ovn-relay-k8s/fetch-libs.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.observability_libs.v1.kubernetes_service_patch -charmcraft fetch-lib charms.ovn_central_k8s.v0.ovsdb -charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates diff --git a/charms/ovn-relay-k8s/lib/charms/observability_libs/v1/kubernetes_service_patch.py b/charms/ovn-relay-k8s/lib/charms/observability_libs/v1/kubernetes_service_patch.py deleted file mode 100644 index 56cca01a..00000000 --- a/charms/ovn-relay-k8s/lib/charms/observability_libs/v1/kubernetes_service_patch.py +++ /dev/null @@ -1,342 +0,0 @@ -# Copyright 2021 Canonical Ltd. -# See LICENSE file for licensing details. - -"""# 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.v1.kubernetes_service_patch -cat << EOF >> requirements.txt -lightkube -lightkube-models -EOF -``` - -Then, to initialise the library: - -For `ClusterIP` services: - -```python -# ... -from charms.observability_libs.v1.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.v1.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.v1.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]) - # ... -``` - -Bound with custom events by providing `refresh_event` argument: -For example, you would like to have a configurable port in your charm and want to apply -service patch every time charm config is changed. - -```python -from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch -from lightkube.models.core_v1 import ServicePort - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - port = ServicePort(int(self.config["charm-config-port"]), name=f"{self.app.name}") - self.service_patcher = KubernetesServicePatch( - self, - [port], - refresh_event=self.on.config_changed - ) - # ... -``` - -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, Optional, Union - -from lightkube import ApiError, Client -from lightkube.core import exceptions -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 BoundEvent, 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 = 6 - -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: Optional[str] = None, - service_type: ServiceType = "ClusterIP", - additional_labels: Optional[dict] = None, - additional_selectors: Optional[dict] = None, - additional_annotations: Optional[dict] = None, - *, - refresh_event: Optional[Union[BoundEvent, List[BoundEvent]]] = 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. - refresh_event: an optional bound event or list of bound events which - will be observed to re-apply the patch (e.g. on port change). - The `install` and `upgrade-charm` events would be observed regardless. - """ - 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) - self.framework.observe(charm.on.update_status, self._patch) - - # apply user defined events - if refresh_event: - if not isinstance(refresh_event, list): - refresh_event = [refresh_event] - - for evt in refresh_event: - self.framework.observe(evt, self._patch) - - def _service_object( - self, - ports: List[ServicePort], - service_name: Optional[str] = None, - service_type: ServiceType = "ClusterIP", - additional_labels: Optional[dict] = None, - additional_selectors: Optional[dict] = None, - additional_annotations: Optional[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. - """ - try: - client = Client() - except exceptions.ConfigError as e: - logger.warning("Error creating k8s client: %s", e) - return - - try: - if self._is_patched(client): - return - 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() - return self._is_patched(client) - - def _is_patched(self, client: Client) -> bool: - # Get the relevant service from the cluster - try: - service = client.get(Service, name=self.service_name, namespace=self._namespace) - except ApiError as e: - if e.status.code == 404 and self.service_name != self._app: - return False - else: - logger.error("Kubernetes service get failed: %s", str(e)) - raise - - # 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/charms/ovn-relay-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py b/charms/ovn-relay-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py deleted file mode 100644 index 732679a6..00000000 --- a/charms/ovn-relay-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py +++ /dev/null @@ -1,206 +0,0 @@ -"""TODO: Add a proper docstring here. - -This is a placeholder docstring for this charm library. Docstrings are -presented on Charmhub and updated whenever you push a new version of the -library. - -Complete documentation about creating and documenting libraries can be found -in the SDK docs at https://juju.is/docs/sdk/libraries. - -See `charmcraft publish-lib` and `charmcraft fetch-lib` for details of how to -share and consume charm libraries. They serve to enhance collaboration -between charmers. Use a charmer's libraries for classes that handle -integration with their charm. - -Bear in mind that new revisions of the different major API versions (v0, v1, -v2 etc) are maintained independently. You can continue to update v0 and v1 -after you have pushed v3. - -Markdown is supported, following the CommonMark specification. -""" - -import logging -import typing -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -# The unique Charmhub library identifier, never change it -LIBID = "114b7bb1970445daa61650e451f9da62" - -# Increment this major API version when introducing breaking changes -LIBAPI = 0 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 3 - - -# TODO: add your code here! Happy coding! -class OVSDBCMSConnectedEvent(EventBase): - """OVSDBCMS connected Event.""" - - pass - - -class OVSDBCMSReadyEvent(EventBase): - """OVSDBCMS ready for use Event.""" - - pass - - -class OVSDBCMSGoneAwayEvent(EventBase): - """OVSDBCMS relation has gone-away Event""" - - pass - - -class OVSDBCMSServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(OVSDBCMSConnectedEvent) - ready = EventSource(OVSDBCMSReadyEvent) - goneaway = EventSource(OVSDBCMSGoneAwayEvent) - - -class OVSDBCMSRequires(Object): - """ - OVSDBCMSRequires class - """ - - on = OVSDBCMSServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_ovsdb_cms_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_ovsdb_cms_relation_broken, - ) - - def _on_ovsdb_cms_relation_joined(self, event): - """OVSDBCMS relation joined.""" - logging.debug("OVSDBCMSRequires on_joined") - self.on.connected.emit() - - def bound_hostnames(self): - return self.get_all_unit_values("bound-hostname") - - def bound_addresses(self): - return self.get_all_unit_values("bound-address") - - def remote_ready(self): - return all(self.bound_hostnames()) or all(self.bound_addresses()) - - def _on_ovsdb_cms_relation_changed(self, event): - """OVSDBCMS relation changed.""" - logging.debug("OVSDBCMSRequires on_changed") - if self.remote_ready(): - self.on.ready.emit() - - def _on_ovsdb_cms_relation_broken(self, event): - """OVSDBCMS relation broken.""" - logging.debug("OVSDBCMSRequires on_broken") - self.on.goneaway.emit() - - def get_all_unit_values(self, key: str) -> typing.List[str]: - """Retrieve value for key from all related units.""" - values = [] - relation = self.framework.model.get_relation(self.relation_name) - if relation: - for unit in relation.units: - values.append(relation.data[unit].get(key)) - return values - - - -class OVSDBCMSClientConnectedEvent(EventBase): - """OVSDBCMS connected Event.""" - - pass - - -class OVSDBCMSClientReadyEvent(EventBase): - """OVSDBCMS ready for use Event.""" - - pass - - -class OVSDBCMSClientGoneAwayEvent(EventBase): - """OVSDBCMS relation has gone-away Event""" - - pass - - -class OVSDBCMSClientEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(OVSDBCMSClientConnectedEvent) - ready = EventSource(OVSDBCMSClientReadyEvent) - goneaway = EventSource(OVSDBCMSClientGoneAwayEvent) - - -class OVSDBCMSProvides(Object): - """ - OVSDBCMSProvides class - """ - - on = OVSDBCMSClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_ovsdb_cms_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_ovsdb_cms_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_ovsdb_cms_relation_broken, - ) - - def _on_ovsdb_cms_relation_joined(self, event): - """Handle ovsdb-cms joined.""" - logging.debug("OVSDBCMSProvides on_joined") - self.on.connected.emit() - - def _on_ovsdb_cms_relation_changed(self, event): - """Handle ovsdb-cms changed.""" - logging.debug("OVSDBCMSProvides on_changed") - self.on.ready.emit() - - def _on_ovsdb_cms_relation_broken(self, event): - """Handle ovsdb-cms broken.""" - logging.debug("OVSDBCMSProvides on_departed") - self.on.goneaway.emit() - - def set_unit_data(self, settings: typing.Dict[str, str]) -> None: - """Publish settings on the peer unit data bag.""" - relations = self.framework.model.relations[self.relation_name] - for relation in relations: - for k, v in settings.items(): - relation.data[self.model.unit][k] = v diff --git a/charms/ovn-relay-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py b/charms/ovn-relay-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py deleted file mode 100644 index 1eda19bf..00000000 --- a/charms/ovn-relay-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py +++ /dev/null @@ -1,1261 +0,0 @@ -# Copyright 2021 Canonical Ltd. -# See LICENSE file for licensing details. - -"""Library for the tls-certificates relation. - -This library contains the Requires and Provides classes for handling the tls-certificates -interface. - -## Getting Started -From a charm directory, fetch the library using `charmcraft`: - -```shell -charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates -``` - -Add the following libraries to the charm's `requirements.txt` file: -- jsonschema -- cryptography - -Add the following section to the charm's `charmcraft.yaml` file: -```yaml -parts: - charm: - build-packages: - - libffi-dev - - libssl-dev - - rustc - - cargo -``` - -### Provider charm -The provider charm is the charm providing certificates to another charm that requires them. In -this example, the provider charm is storing its private key using a peer relation interface called -`replicas`. - -Example: -```python -from charms.tls_certificates_interface.v1.tls_certificates import ( - CertificateCreationRequestEvent, - CertificateRevocationRequestEvent, - TLSCertificatesProvidesV1, - generate_private_key, -) -from ops.charm import CharmBase, InstallEvent -from ops.main import main -from ops.model import ActiveStatus, WaitingStatus - - -def generate_ca(private_key: bytes, subject: str) -> str: - return "whatever ca content" - - -def generate_certificate(ca: str, private_key: str, csr: str) -> str: - return "Whatever certificate" - - -class ExampleProviderCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - self.certificates = TLSCertificatesProvidesV1(self, "certificates") - self.framework.observe( - self.certificates.on.certificate_request, self._on_certificate_request - ) - self.framework.observe( - self.certificates.on.certificate_revoked, self._on_certificate_revocation_request - ) - self.framework.observe(self.on.install, self._on_install) - - def _on_install(self, event: InstallEvent) -> None: - private_key_password = b"banana" - private_key = generate_private_key(password=private_key_password) - ca_certificate = generate_ca(private_key=private_key, subject="whatever") - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update( - { - "private_key_password": "banana", - "private_key": private_key, - "ca_certificate": ca_certificate, - } - ) - self.unit.status = ActiveStatus() - - def _on_certificate_request(self, event: CertificateCreationRequestEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - ca_certificate = replicas_relation.data[self.app].get("ca_certificate") - private_key = replicas_relation.data[self.app].get("private_key") - certificate = generate_certificate( - ca=ca_certificate, - private_key=private_key, - csr=event.certificate_signing_request, - ) - - self.certificates.set_relation_certificate( - certificate=certificate, - certificate_signing_request=event.certificate_signing_request, - ca=ca_certificate, - chain=[ca_certificate, certificate], - relation_id=event.relation_id, - ) - - def _on_certificate_revocation_request(self, event: CertificateRevocationRequestEvent) -> None: - # Do what you want to do with this information - pass - - -if __name__ == "__main__": - main(ExampleProviderCharm) -``` - -### Requirer charm -The requirer charm is the charm requiring certificates from another charm that provides them. In -this example, the requirer charm is storing its certificates using a peer relation interface called -`replicas`. - -Example: -```python -from charms.tls_certificates_interface.v1.tls_certificates import ( - CertificateAvailableEvent, - CertificateExpiringEvent, - TLSCertificatesRequiresV1, - generate_csr, - generate_private_key, -) -from ops.charm import CharmBase, RelationJoinedEvent -from ops.main import main -from ops.model import ActiveStatus, WaitingStatus - - -class ExampleRequirerCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - self.cert_subject = "whatever" - self.certificates = TLSCertificatesRequiresV1(self, "certificates") - self.framework.observe(self.on.install, self._on_install) - self.framework.observe( - self.on.certificates_relation_joined, self._on_certificates_relation_joined - ) - self.framework.observe( - self.certificates.on.certificate_available, self._on_certificate_available - ) - self.framework.observe( - self.certificates.on.certificate_expiring, self._on_certificate_expiring - ) - - def _on_install(self, event) -> None: - private_key_password = b"banana" - private_key = generate_private_key(password=private_key_password) - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update( - {"private_key_password": "banana", "private_key": private_key.decode()} - ) - - def _on_certificates_relation_joined(self, event: RelationJoinedEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - replicas_relation.data[self.app].update({"csr": csr.decode()}) - self.certificates.request_certificate_creation(certificate_signing_request=csr) - - def _on_certificate_available(self, event: CertificateAvailableEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - replicas_relation.data[self.app].update({"certificate": event.certificate}) - replicas_relation.data[self.app].update({"ca": event.ca}) - replicas_relation.data[self.app].update({"chain": event.chain}) - self.unit.status = ActiveStatus() - - def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None: - replicas_relation = self.model.get_relation("replicas") - if not replicas_relation: - self.unit.status = WaitingStatus("Waiting for peer relation to be created") - event.defer() - return - old_csr = replicas_relation.data[self.app].get("csr") - private_key_password = replicas_relation.data[self.app].get("private_key_password") - private_key = replicas_relation.data[self.app].get("private_key") - new_csr = generate_csr( - private_key=private_key.encode(), - private_key_password=private_key_password.encode(), - subject=self.cert_subject, - ) - self.certificates.request_certificate_renewal( - old_certificate_signing_request=old_csr, - new_certificate_signing_request=new_csr, - ) - replicas_relation.data[self.app].update({"csr": new_csr.decode()}) - - -if __name__ == "__main__": - main(ExampleRequirerCharm) -``` -""" # noqa: D405, D410, D411, D214, D416 - -import copy -import json -import logging -import uuid -from datetime import datetime, timedelta -from ipaddress import IPv4Address -from typing import Dict, List, Optional - -from cryptography import x509 -from cryptography.hazmat._oid import ExtensionOID -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.hazmat.primitives.serialization import pkcs12 -from cryptography.x509.extensions import Extension, ExtensionNotFound -from jsonschema import exceptions, validate # type: ignore[import] -from ops.charm import CharmBase, CharmEvents, RelationChangedEvent, UpdateStatusEvent -from ops.framework import EventBase, EventSource, Handle, Object - -# The unique Charmhub library identifier, never change it -LIBID = "afd8c2bccf834997afce12c2706d2ede" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 10 - -REQUIRER_JSON_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/requirer.json", # noqa: E501 - "type": "object", - "title": "`tls_certificates` requirer root schema", - "description": "The `tls_certificates` root schema comprises the entire requirer databag for this interface.", # noqa: E501 - "examples": [ - { - "certificate_signing_requests": [ - { - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\\n-----END CERTIFICATE REQUEST-----\\n" # noqa: E501 - }, - { - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBAMk3raaX803cHvzlBF9LC7KORT46z4VjyU5PIaMb\\nQLIDgYKFYI0n5hf2Ra4FAHvOvEmW7bjNlHORFEmvnpcU5kPMNUyKFMTaC8LGmN8z\\nUBH3aK+0+FRvY4afn9tgj5435WqOG9QdoDJ0TJkjJbJI9M70UOgL711oU7ql6HxU\\n4d2ydFK9xAHrBwziNHgNZ72L95s4gLTXf0fAHYf15mDA9U5yc+YDubCKgTXzVySQ\\nUx73VCJLfC/XkZIh559IrnRv5G9fu6BMLEuBwAz6QAO4+/XidbKWN4r2XSq5qX4n\\n6EPQQWP8/nd4myq1kbg6Q8w68L/0YdfjCmbyf2TuoWeImdUCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQBIdwraBvpYo/rl5MH1+1Um6HRg4gOdQPY5WcJy9B9tgzJz\\nittRSlRGTnhyIo6fHgq9KHrmUthNe8mMTDailKFeaqkVNVvk7l0d1/B90Kz6OfmD\\nxN0qjW53oP7y3QB5FFBM8DjqjmUnz5UePKoX4AKkDyrKWxMwGX5RoET8c/y0y9jp\\nvSq3Wh5UpaZdWbe1oVY8CqMVUEVQL2DPjtopxXFz2qACwsXkQZxWmjvZnRiP8nP8\\nbdFaEuh9Q6rZ2QdZDEtrU4AodPU3NaukFr5KlTUQt3w/cl+5//zils6G5zUWJ2pN\\ng7+t9PTvXHRkH+LnwaVnmsBFU2e05qADQbfIn7JA\\n-----END CERTIFICATE REQUEST-----\\n" # noqa: E501 - }, - ] - } - ], - "properties": { - "certificate_signing_requests": { - "type": "array", - "items": { - "type": "object", - "properties": {"certificate_signing_request": {"type": "string"}}, - "required": ["certificate_signing_request"], - }, - } - }, - "required": ["certificate_signing_requests"], - "additionalProperties": True, -} - -PROVIDER_JSON_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "$id": "https://canonical.github.io/charm-relation-interfaces/tls_certificates/v1/schemas/provider.json", # noqa: E501 - "type": "object", - "title": "`tls_certificates` provider root schema", - "description": "The `tls_certificates` root schema comprises the entire provider databag for this interface.", # noqa: E501 - "example": [ - { - "certificates": [ - { - "ca": "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n", # noqa: E501 - "chain": [ - "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n" # noqa: E501, W505 - ], - "certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\n-----END CERTIFICATE REQUEST-----\n", # noqa: E501 - "certificate": "-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n", # noqa: E501 - } - ] - } - ], - "properties": { - "certificates": { - "$id": "#/properties/certificates", - "type": "array", - "items": { - "$id": "#/properties/certificates/items", - "type": "object", - "required": ["certificate_signing_request", "certificate", "ca", "chain"], - "properties": { - "certificate_signing_request": { - "$id": "#/properties/certificates/items/certificate_signing_request", - "type": "string", - }, - "certificate": { - "$id": "#/properties/certificates/items/certificate", - "type": "string", - }, - "ca": {"$id": "#/properties/certificates/items/ca", "type": "string"}, - "chain": { - "$id": "#/properties/certificates/items/chain", - "type": "array", - "items": { - "type": "string", - "$id": "#/properties/certificates/items/chain/items", - }, - }, - }, - "additionalProperties": True, - }, - } - }, - "required": ["certificates"], - "additionalProperties": True, -} - - -logger = logging.getLogger(__name__) - - -class CertificateAvailableEvent(EventBase): - """Charm Event triggered when a TLS certificate is available.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - - -class CertificateExpiringEvent(EventBase): - """Charm Event triggered when a TLS certificate is almost expired.""" - - def __init__(self, handle, certificate: str, expiry: str): - """CertificateExpiringEvent. - - Args: - handle (Handle): Juju framework handle - certificate (str): TLS Certificate - expiry (str): Datetime string reprensenting the time at which the certificate - won't be valid anymore. - """ - super().__init__(handle) - self.certificate = certificate - self.expiry = expiry - - def snapshot(self) -> dict: - """Returns snapshot.""" - return {"certificate": self.certificate, "expiry": self.expiry} - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.expiry = snapshot["expiry"] - - -class CertificateExpiredEvent(EventBase): - """Charm Event triggered when a TLS certificate is expired.""" - - def __init__(self, handle: Handle, certificate: str): - super().__init__(handle) - self.certificate = certificate - - def snapshot(self) -> dict: - """Returns snapshot.""" - return {"certificate": self.certificate} - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - - -class CertificateCreationRequestEvent(EventBase): - """Charm Event triggered when a TLS certificate is required.""" - - def __init__(self, handle: Handle, certificate_signing_request: str, relation_id: int): - super().__init__(handle) - self.certificate_signing_request = certificate_signing_request - self.relation_id = relation_id - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate_signing_request": self.certificate_signing_request, - "relation_id": self.relation_id, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.relation_id = snapshot["relation_id"] - - -class CertificateRevocationRequestEvent(EventBase): - """Charm Event triggered when a TLS certificate needs to be revoked.""" - - def __init__( - self, - handle: Handle, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: str, - ): - super().__init__(handle) - self.certificate = certificate - self.certificate_signing_request = certificate_signing_request - self.ca = ca - self.chain = chain - - def snapshot(self) -> dict: - """Returns snapshot.""" - return { - "certificate": self.certificate, - "certificate_signing_request": self.certificate_signing_request, - "ca": self.ca, - "chain": self.chain, - } - - def restore(self, snapshot: dict): - """Restores snapshot.""" - self.certificate = snapshot["certificate"] - self.certificate_signing_request = snapshot["certificate_signing_request"] - self.ca = snapshot["ca"] - self.chain = snapshot["chain"] - - -def _load_relation_data(raw_relation_data: dict) -> dict: - """Loads relation data from the relation data bag. - - Json loads all data. - - Args: - raw_relation_data: Relation data from the databag - - Returns: - dict: Relation data in dict format. - """ - certificate_data = dict() - for key in raw_relation_data: - try: - certificate_data[key] = json.loads(raw_relation_data[key]) - except (json.decoder.JSONDecodeError, TypeError): - certificate_data[key] = raw_relation_data[key] - return certificate_data - - -def generate_ca( - private_key: bytes, - subject: str, - private_key_password: Optional[bytes] = None, - validity: int = 365, - country: str = "US", -) -> bytes: - """Generates a CA Certificate. - - Args: - private_key (bytes): Private key - subject (str): Certificate subject - private_key_password (bytes): Private key password - validity (int): Certificate validity time (in days) - country (str): Certificate Issuing country - - Returns: - bytes: CA Certificate. - """ - private_key_object = serialization.load_pem_private_key( - private_key, password=private_key_password - ) - subject = issuer = x509.Name( - [ - x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country), - x509.NameAttribute(x509.NameOID.COMMON_NAME, subject), - ] - ) - subject_identifier_object = x509.SubjectKeyIdentifier.from_public_key( - private_key_object.public_key() # type: ignore[arg-type] - ) - subject_identifier = key_identifier = subject_identifier_object.public_bytes() - cert = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(private_key_object.public_key()) # type: ignore[arg-type] - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.utcnow()) - .not_valid_after(datetime.utcnow() + timedelta(days=validity)) - .add_extension(x509.SubjectKeyIdentifier(digest=subject_identifier), critical=False) - .add_extension( - x509.AuthorityKeyIdentifier( - key_identifier=key_identifier, - authority_cert_issuer=None, - authority_cert_serial_number=None, - ), - critical=False, - ) - .add_extension( - x509.BasicConstraints(ca=True, path_length=None), - critical=True, - ) - .sign(private_key_object, hashes.SHA256()) # type: ignore[arg-type] - ) - return cert.public_bytes(serialization.Encoding.PEM) - - -def generate_certificate( - csr: bytes, - ca: bytes, - ca_key: bytes, - ca_key_password: Optional[bytes] = None, - validity: int = 365, - alt_names: List[str] = None, -) -> bytes: - """Generates a TLS certificate based on a CSR. - - Args: - csr (bytes): CSR - ca (bytes): CA Certificate - ca_key (bytes): CA private key - ca_key_password: CA private key password - validity (int): Certificate validity (in days) - alt_names (list): List of alt names to put on cert - prefer putting SANs in CSR - - Returns: - bytes: Certificate - """ - csr_object = x509.load_pem_x509_csr(csr) - subject = csr_object.subject - issuer = x509.load_pem_x509_certificate(ca).issuer - private_key = serialization.load_pem_private_key(ca_key, password=ca_key_password) - - certificate_builder = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(csr_object.public_key()) - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.utcnow()) - .not_valid_after(datetime.utcnow() + timedelta(days=validity)) - ) - - extensions_list = csr_object.extensions - san_ext: Optional[x509.Extension] = None - if alt_names: - full_sans_dns = alt_names.copy() - try: - loaded_san_ext = csr_object.extensions.get_extension_for_class( - x509.SubjectAlternativeName - ) - full_sans_dns.extend(loaded_san_ext.value.get_values_for_type(x509.DNSName)) - except ExtensionNotFound: - pass - finally: - san_ext = Extension( - ExtensionOID.SUBJECT_ALTERNATIVE_NAME, - False, - x509.SubjectAlternativeName([x509.DNSName(name) for name in full_sans_dns]), - ) - if not extensions_list: - extensions_list = x509.Extensions([san_ext]) - - for extension in extensions_list: - if extension.value.oid == ExtensionOID.SUBJECT_ALTERNATIVE_NAME and san_ext: - extension = san_ext - - certificate_builder = certificate_builder.add_extension( - extension.value, - critical=extension.critical, - ) - certificate_builder._version = x509.Version.v3 - cert = certificate_builder.sign(private_key, hashes.SHA256()) # type: ignore[arg-type] - return cert.public_bytes(serialization.Encoding.PEM) - - -def generate_pfx_package( - certificate: bytes, - private_key: bytes, - package_password: str, - private_key_password: Optional[bytes] = None, -) -> bytes: - """Generates a PFX package to contain the TLS certificate and private key. - - Args: - certificate (bytes): TLS certificate - private_key (bytes): Private key - package_password (str): Password to open the PFX package - private_key_password (bytes): Private key password - - Returns: - bytes: - """ - private_key_object = serialization.load_pem_private_key( - private_key, password=private_key_password - ) - certificate_object = x509.load_pem_x509_certificate(certificate) - name = certificate_object.subject.rfc4514_string() - pfx_bytes = pkcs12.serialize_key_and_certificates( - name=name.encode(), - cert=certificate_object, - key=private_key_object, # type: ignore[arg-type] - cas=None, - encryption_algorithm=serialization.BestAvailableEncryption(package_password.encode()), - ) - return pfx_bytes - - -def generate_private_key( - password: Optional[bytes] = None, - key_size: int = 2048, - public_exponent: int = 65537, -) -> bytes: - """Generates a private key. - - Args: - password (bytes): Password for decrypting the private key - key_size (int): Key size in bytes - public_exponent: Public exponent. - - Returns: - bytes: Private Key - """ - private_key = rsa.generate_private_key( - public_exponent=public_exponent, - key_size=key_size, - ) - key_bytes = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.BestAvailableEncryption(password) - if password - else serialization.NoEncryption(), - ) - return key_bytes - - -def generate_csr( - private_key: bytes, - subject: str, - add_unique_id_to_subject_name: bool = True, - organization: str = None, - email_address: str = None, - country_name: str = None, - private_key_password: Optional[bytes] = None, - sans: Optional[List[str]] = None, - sans_oid: Optional[List[str]] = None, - sans_ip: Optional[List[str]] = None, - sans_dns: Optional[List[str]] = None, - additional_critical_extensions: Optional[List] = None, -) -> bytes: - """Generates a CSR using private key and subject. - - Args: - private_key (bytes): Private key - subject (str): CSR Subject. - add_unique_id_to_subject_name (bool): Whether a unique ID must be added to the CSR's - subject name. Always leave to "True" when the CSR is used to request certificates - using the tls-certificates relation. - organization (str): Name of organization. - email_address (str): Email address. - country_name (str): Country Name. - private_key_password (bytes): Private key password - sans (list): Use sans_dns - this will be deprecated in a future release - List of DNS subject alternative names (keeping it for now for backward compatibility) - sans_oid (list): List of registered ID SANs - sans_dns (list): List of DNS subject alternative names (similar to the arg: sans) - sans_ip (list): List of IP subject alternative names - additional_critical_extensions (list): List if critical additional extension objects. - Object must be a x509 ExtensionType. - - Returns: - bytes: CSR - """ - signing_key = serialization.load_pem_private_key(private_key, password=private_key_password) - subject_name = [x509.NameAttribute(x509.NameOID.COMMON_NAME, subject)] - if add_unique_id_to_subject_name: - unique_identifier = uuid.uuid4() - subject_name.append( - x509.NameAttribute(x509.NameOID.X500_UNIQUE_IDENTIFIER, str(unique_identifier)) - ) - if organization: - subject_name.append(x509.NameAttribute(x509.NameOID.ORGANIZATION_NAME, organization)) - if email_address: - subject_name.append(x509.NameAttribute(x509.NameOID.EMAIL_ADDRESS, email_address)) - if country_name: - subject_name.append(x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country_name)) - csr = x509.CertificateSigningRequestBuilder(subject_name=x509.Name(subject_name)) - - _sans: List[x509.GeneralName] = [] - if sans_oid: - _sans.extend([x509.RegisteredID(x509.ObjectIdentifier(san)) for san in sans_oid]) - if sans_ip: - _sans.extend([x509.IPAddress(IPv4Address(san)) for san in sans_ip]) - if sans: - _sans.extend([x509.DNSName(san) for san in sans]) - if sans_dns: - _sans.extend([x509.DNSName(san) for san in sans_dns]) - if _sans: - csr = csr.add_extension(x509.SubjectAlternativeName(set(_sans)), critical=False) - - if additional_critical_extensions: - for extension in additional_critical_extensions: - csr = csr.add_extension(extension, critical=True) - - signed_certificate = csr.sign(signing_key, hashes.SHA256()) # type: ignore[arg-type] - return signed_certificate.public_bytes(serialization.Encoding.PEM) - - -class CertificatesProviderCharmEvents(CharmEvents): - """List of events that the TLS Certificates provider charm can leverage.""" - - certificate_creation_request = EventSource(CertificateCreationRequestEvent) - certificate_revocation_request = EventSource(CertificateRevocationRequestEvent) - - -class CertificatesRequirerCharmEvents(CharmEvents): - """List of events that the TLS Certificates requirer charm can leverage.""" - - certificate_available = EventSource(CertificateAvailableEvent) - certificate_expiring = EventSource(CertificateExpiringEvent) - certificate_expired = EventSource(CertificateExpiredEvent) - - -class TLSCertificatesProvidesV1(Object): - """TLS certificates provider class to be instantiated by TLS certificates providers.""" - - on = CertificatesProviderCharmEvents() - - def __init__(self, charm: CharmBase, relationship_name: str): - super().__init__(charm, relationship_name) - self.framework.observe( - charm.on[relationship_name].relation_changed, self._on_relation_changed - ) - self.charm = charm - self.relationship_name = relationship_name - - def _add_certificate( - self, - relation_id: int, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - ) -> None: - """Adds certificate to relation data. - - Args: - relation_id (int): Relation id - certificate (str): Certificate - certificate_signing_request (str): Certificate Signing Request - ca (str): CA Certificate - chain (list): CA Chain - - Returns: - None - """ - relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - new_certificate = { - "certificate": certificate, - "certificate_signing_request": certificate_signing_request, - "ca": ca, - "chain": chain, - } - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = provider_relation_data.get("certificates", []) - certificates = copy.deepcopy(provider_certificates) - if new_certificate in certificates: - logger.info("Certificate already in relation data - Doing nothing") - return - certificates.append(new_certificate) - relation.data[self.model.app]["certificates"] = json.dumps(certificates) - - def _remove_certificate( - self, - relation_id: int, - certificate: str = None, - certificate_signing_request: str = None, - ) -> None: - """Removes certificate from a given relation based on user provided certificate or csr. - - Args: - relation_id (int): Relation id - certificate (str): Certificate (optional) - certificate_signing_request: Certificate signing request (optional) - - Returns: - None - """ - relation = self.model.get_relation( - relation_name=self.relationship_name, - relation_id=relation_id, - ) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} with relation id {relation_id} does not exist" - ) - provider_relation_data = _load_relation_data(relation.data[self.charm.app]) - provider_certificates = provider_relation_data.get("certificates", []) - certificates = copy.deepcopy(provider_certificates) - for certificate_dict in certificates: - if certificate and certificate_dict["certificate"] == certificate: - certificates.remove(certificate_dict) - if ( - certificate_signing_request - and certificate_dict["certificate_signing_request"] == certificate_signing_request - ): - certificates.remove(certificate_dict) - relation.data[self.model.app]["certificates"] = json.dumps(certificates) - - @staticmethod - def _relation_data_is_valid(certificates_data: dict) -> bool: - """Uses JSON schema validator to validate relation data content. - - Args: - certificates_data (dict): Certificate data dictionary as retrieved from relation data. - - Returns: - bool: True/False depending on whether the relation data follows the json schema. - """ - try: - validate(instance=certificates_data, schema=REQUIRER_JSON_SCHEMA) - return True - except exceptions.ValidationError: - return False - - def revoke_all_certificates(self) -> None: - """Revokes all certificates of this provider. - - This method is meant to be used when the Root CA has changed. - """ - for relation in self.model.relations[self.relationship_name]: - relation.data[self.model.app]["certificates"] = json.dumps([]) - - def set_relation_certificate( - self, - certificate: str, - certificate_signing_request: str, - ca: str, - chain: List[str], - relation_id: int, - ) -> None: - """Adds certificates to relation data. - - Args: - certificate (str): Certificate - certificate_signing_request (str): Certificate signing request - ca (str): CA Certificate - chain (list): CA Chain - relation_id (int): Juju relation ID - - Returns: - None - """ - certificates_relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - self._remove_certificate( - certificate_signing_request=certificate_signing_request.strip(), - relation_id=relation_id, - ) - self._add_certificate( - relation_id=relation_id, - certificate=certificate.strip(), - certificate_signing_request=certificate_signing_request.strip(), - ca=ca.strip(), - chain=[cert.strip() for cert in chain], - ) - - def remove_certificate(self, certificate: str) -> None: - """Removes a given certificate from relation data. - - Args: - certificate (str): TLS Certificate - - Returns: - None - """ - certificates_relation = self.model.relations[self.relationship_name] - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - for certificate_relation in certificates_relation: - self._remove_certificate(certificate=certificate, relation_id=certificate_relation.id) - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handler triggerred on relation changed event. - - Looks at the relation data and either emits: - - certificate request event: If the unit relation data contains a CSR for which - a certificate does not exist in the provider relation data. - - certificate revocation event: If the provider relation data contains a CSR for which - a csr does not exist in the requirer relation data. - - Args: - event: Juju event - - Returns: - None - """ - assert event.unit is not None - requirer_relation_data = _load_relation_data(event.relation.data[event.unit]) - provider_relation_data = _load_relation_data(event.relation.data[self.charm.app]) - if not self._relation_data_is_valid(requirer_relation_data): - logger.warning( - f"Relation data did not pass JSON Schema validation: {requirer_relation_data}" - ) - return - provider_certificates = provider_relation_data.get("certificates", []) - requirer_csrs = requirer_relation_data.get("certificate_signing_requests", []) - provider_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in provider_certificates - ] - requirer_unit_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in requirer_csrs - ] - for certificate_signing_request in requirer_unit_csrs: - if certificate_signing_request not in provider_csrs: - self.on.certificate_creation_request.emit( - certificate_signing_request=certificate_signing_request, - relation_id=event.relation.id, - ) - self._revoke_certificates_for_which_no_csr_exists(relation_id=event.relation.id) - - def _revoke_certificates_for_which_no_csr_exists(self, relation_id: int) -> None: - """Revokes certificates for which no unit has a CSR. - - Goes through all generated certificates and compare agains the list of CSRS for all units - of a given relationship. - - Args: - relation_id (int): Relation id - - Returns: - None - """ - certificates_relation = self.model.get_relation( - relation_name=self.relationship_name, relation_id=relation_id - ) - if not certificates_relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - provider_relation_data = _load_relation_data(certificates_relation.data[self.charm.app]) - list_of_csrs: List[str] = [] - for unit in certificates_relation.units: - requirer_relation_data = _load_relation_data(certificates_relation.data[unit]) - requirer_csrs = requirer_relation_data.get("certificate_signing_requests", []) - list_of_csrs.extend(csr["certificate_signing_request"] for csr in requirer_csrs) - provider_certificates = provider_relation_data.get("certificates", []) - for certificate in provider_certificates: - if certificate["certificate_signing_request"] not in list_of_csrs: - self.on.certificate_revocation_request.emit( - certificate=certificate["certificate"], - certificate_signing_request=certificate["certificate_signing_request"], - ca=certificate["ca"], - chain=certificate["chain"], - ) - self.remove_certificate(certificate=certificate["certificate"]) - - -class TLSCertificatesRequiresV1(Object): - """TLS certificates requirer class to be instantiated by TLS certificates requirers.""" - - on = CertificatesRequirerCharmEvents() - - def __init__( - self, - charm: CharmBase, - relationship_name: str, - expiry_notification_time: int = 168, - ): - """Generates/use private key and observes relation changed event. - - Args: - charm: Charm object - relationship_name: Juju relation name - expiry_notification_time (int): Time difference between now and expiry (in hours). - Used to trigger the CertificateExpiring event. Default: 7 days. - """ - super().__init__(charm, relationship_name) - self.relationship_name = relationship_name - self.charm = charm - self.expiry_notification_time = expiry_notification_time - self.framework.observe( - charm.on[relationship_name].relation_changed, self._on_relation_changed - ) - self.framework.observe(charm.on.update_status, self._on_update_status) - - @property - def _requirer_csrs(self) -> List[Dict[str, str]]: - """Returns list of requirer CSR's from relation data.""" - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - requirer_relation_data = _load_relation_data(relation.data[self.model.unit]) - return requirer_relation_data.get("certificate_signing_requests", []) - - @property - def _provider_certificates(self) -> List[Dict[str, str]]: - """Returns list of provider CSR's from relation data.""" - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError(f"Relation {self.relationship_name} does not exist") - if not relation.app: - raise RuntimeError(f"Remote app for relation {self.relationship_name} does not exist") - provider_relation_data = _load_relation_data(relation.data[relation.app]) - return provider_relation_data.get("certificates", []) - - def _add_requirer_csr(self, csr: str) -> None: - """Adds CSR to relation data. - - Args: - csr (str): Certificate Signing Request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - new_csr_dict = {"certificate_signing_request": csr} - if new_csr_dict in self._requirer_csrs: - logger.info("CSR already in relation data - Doing nothing") - return - requirer_csrs = copy.deepcopy(self._requirer_csrs) - requirer_csrs.append(new_csr_dict) - relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs) - - def _remove_requirer_csr(self, csr: str) -> None: - """Removes CSR from relation data. - - Args: - csr (str): Certificate signing request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - raise RuntimeError( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - requirer_csrs = copy.deepcopy(self._requirer_csrs) - csr_dict = {"certificate_signing_request": csr} - if csr_dict not in requirer_csrs: - logger.info("CSR not in relation data - Doing nothing") - return - requirer_csrs.remove(csr_dict) - relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(requirer_csrs) - - def request_certificate_creation(self, certificate_signing_request: bytes) -> None: - """Request TLS certificate to provider charm. - - Args: - certificate_signing_request (bytes): Certificate Signing Request - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - message = ( - f"Relation {self.relationship_name} does not exist - " - f"The certificate request can't be completed" - ) - logger.error(message) - raise RuntimeError(message) - self._add_requirer_csr(certificate_signing_request.decode().strip()) - logger.info("Certificate request sent to provider") - - def request_certificate_revocation(self, certificate_signing_request: bytes) -> None: - """Removes CSR from relation data. - - The provider of this relation is then expected to remove certificates associated to this - CSR from the relation data as well and emit a request_certificate_revocation event for the - provider charm to interpret. - - Args: - certificate_signing_request (bytes): Certificate Signing Request - - Returns: - None - """ - self._remove_requirer_csr(certificate_signing_request.decode().strip()) - logger.info("Certificate revocation sent to provider") - - def request_certificate_renewal( - self, old_certificate_signing_request: bytes, new_certificate_signing_request: bytes - ) -> None: - """Renews certificate. - - Removes old CSR from relation data and adds new one. - - Args: - old_certificate_signing_request: Old CSR - new_certificate_signing_request: New CSR - - Returns: - None - """ - try: - self.request_certificate_revocation( - certificate_signing_request=old_certificate_signing_request - ) - except RuntimeError: - logger.warning("Certificate revocation failed.") - self.request_certificate_creation( - certificate_signing_request=new_certificate_signing_request - ) - logger.info("Certificate renewal request completed.") - - @staticmethod - def _relation_data_is_valid(certificates_data: dict) -> bool: - """Checks whether relation data is valid based on json schema. - - Args: - certificates_data: Certificate data in dict format. - - Returns: - bool: Whether relation data is valid. - """ - try: - validate(instance=certificates_data, schema=PROVIDER_JSON_SCHEMA) - return True - except exceptions.ValidationError: - return False - - def _on_relation_changed(self, event: RelationChangedEvent) -> None: - """Handler triggerred on relation changed events. - - Args: - event: Juju event - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - logger.warning(f"No relation: {self.relationship_name}") - return - if not relation.app: - logger.warning(f"No remote app in relation: {self.relationship_name}") - return - provider_relation_data = _load_relation_data(relation.data[relation.app]) - if not self._relation_data_is_valid(provider_relation_data): - logger.warning( - f"Provider relation data did not pass JSON Schema validation: " - f"{event.relation.data[relation.app]}" - ) - return - requirer_csrs = [ - certificate_creation_request["certificate_signing_request"] - for certificate_creation_request in self._requirer_csrs - ] - for certificate in self._provider_certificates: - if certificate["certificate_signing_request"] in requirer_csrs: - self.on.certificate_available.emit( - certificate_signing_request=certificate["certificate_signing_request"], - certificate=certificate["certificate"], - ca=certificate["ca"], - chain=certificate["chain"], - ) - - def _on_update_status(self, event: UpdateStatusEvent) -> None: - """Triggered on update status event. - - Goes through each certificate in the "certificates" relation and checks their expiry date. - If they are close to expire (<7 days), emits a CertificateExpiringEvent event and if - they are expired, emits a CertificateExpiredEvent. - - Args: - event (UpdateStatusEvent): Juju event - - Returns: - None - """ - relation = self.model.get_relation(self.relationship_name) - if not relation: - logger.warning(f"No relation: {self.relationship_name}") - return - if not relation.app: - logger.warning(f"No remote app in relation: {self.relationship_name}") - return - provider_relation_data = _load_relation_data(relation.data[relation.app]) - if not self._relation_data_is_valid(provider_relation_data): - logger.warning( - f"Provider relation data did not pass JSON Schema validation: " - f"{relation.data[relation.app]}" - ) - return - for certificate_dict in self._provider_certificates: - certificate = certificate_dict["certificate"] - try: - certificate_object = x509.load_pem_x509_certificate(data=certificate.encode()) - except ValueError: - logger.warning("Could not load certificate.") - continue - time_difference = certificate_object.not_valid_after - datetime.utcnow() - if time_difference.total_seconds() < 0: - logger.warning("Certificate is expired") - self.on.certificate_expired.emit(certificate=certificate) - self.request_certificate_revocation(certificate.encode()) - continue - if time_difference.total_seconds() < (self.expiry_notification_time * 60 * 60): - logger.warning("Certificate almost expired") - self.on.certificate_expiring.emit( - certificate=certificate, expiry=certificate_object.not_valid_after.isoformat() - ) diff --git a/charms/ovn-relay-k8s/osci.yaml b/charms/ovn-relay-k8s/osci.yaml deleted file mode 100644 index 352efb71..00000000 --- a/charms/ovn-relay-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: ovn-relay-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 23.09/edge diff --git a/charms/ovn-relay-k8s/pyproject.toml b/charms/ovn-relay-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/ovn-relay-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/ovn-relay-k8s/rename.sh b/charms/ovn-relay-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/ovn-relay-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/ovn-relay-k8s/requirements.txt b/charms/ovn-relay-k8s/requirements.txt index 9211d667..ddbedec1 100644 --- a/charms/ovn-relay-k8s/requirements.txt +++ b/charms/ovn-relay-k8s/requirements.txt @@ -11,4 +11,5 @@ lightkube lightkube-models ops -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam +# From ops_sunbeam +tenacity diff --git a/charms/ovn-relay-k8s/test-requirements.txt b/charms/ovn-relay-k8s/test-requirements.txt deleted file mode 100644 index da3b04b7..00000000 --- a/charms/ovn-relay-k8s/test-requirements.txt +++ /dev/null @@ -1,12 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -coverage -mock -stestr -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/ovn-relay-k8s/tests/unit/test_ovn_relay_charm.py b/charms/ovn-relay-k8s/tests/unit/test_ovn_relay_charm.py index f4654d66..475e4d55 100644 --- a/charms/ovn-relay-k8s/tests/unit/test_ovn_relay_charm.py +++ b/charms/ovn-relay-k8s/tests/unit/test_ovn_relay_charm.py @@ -16,9 +16,8 @@ """Tests for OVN relay.""" -import ops_sunbeam.test_utils as test_utils - import charm +import ops_sunbeam.test_utils as test_utils class _OVNRelayOperatorCharm(charm.OVNRelayOperatorCharm): diff --git a/charms/ovn-relay-k8s/tox.ini b/charms/ovn-relay-k8s/tox.ini deleted file mode 100644 index b0d1dd7b..00000000 --- a/charms/ovn-relay-k8s/tox.ini +++ /dev/null @@ -1,169 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - PYTHONPATH - HOME -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504 diff --git a/charms/placement-k8s/.flake8 b/charms/placement-k8s/.flake8 deleted file mode 100644 index 8ef84fcd..00000000 --- a/charms/placement-k8s/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 99 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/charms/placement-k8s/.gitignore b/charms/placement-k8s/.gitignore deleted file mode 100644 index 73f116c9..00000000 --- a/charms/placement-k8s/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -venv/ -build/ -*.charm -*.swp - -.coverage -__pycache__/ -*.py[cod] -.tox -.stestr/ -tempest.log diff --git a/charms/placement-k8s/.gitreview b/charms/placement-k8s/.gitreview deleted file mode 100644 index 5d944736..00000000 --- a/charms/placement-k8s/.gitreview +++ /dev/null @@ -1,5 +0,0 @@ -[gerrit] -host=review.opendev.org -port=29418 -project=openstack/charm-placement-k8s.git -defaultbranch=main diff --git a/charms/placement-k8s/.jujuignore b/charms/placement-k8s/.jujuignore deleted file mode 100644 index 6ccd559e..00000000 --- a/charms/placement-k8s/.jujuignore +++ /dev/null @@ -1,3 +0,0 @@ -/venv -*.py[cod] -*.charm diff --git a/charms/placement-k8s/.stestr.conf b/charms/placement-k8s/.stestr.conf deleted file mode 100644 index e4750de4..00000000 --- a/charms/placement-k8s/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/charms/placement-k8s/.zuul.yaml b/charms/placement-k8s/.zuul.yaml deleted file mode 100644 index 5f56197e..00000000 --- a/charms/placement-k8s/.zuul.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- project: - templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: placement-k8s - juju_channel: 3.1/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false diff --git a/charms/placement-k8s/charmcraft.yaml b/charms/placement-k8s/charmcraft.yaml index 2fdc318d..9556149e 100644 --- a/charms/placement-k8s/charmcraft.yaml +++ b/charms/placement-k8s/charmcraft.yaml @@ -28,4 +28,3 @@ parts: - jsonschema - pydantic<2.0 - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/placement-k8s/fetch-libs.sh b/charms/placement-k8s/fetch-libs.sh deleted file mode 100755 index e7772471..00000000 --- a/charms/placement-k8s/fetch-libs.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -charmcraft fetch-lib charms.data_platform_libs.v0.database_requires -charmcraft fetch-lib charms.keystone_k8s.v1.identity_service -charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.traefik_k8s.v2.ingress diff --git a/charms/placement-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/placement-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 53d61912..00000000 --- a/charms/placement-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,496 +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. - -"""Relation 'requires' side abstraction for database relation. - -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.database_requires import 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.database_requires import 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, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(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 password(self) -> Optional[str]: - """Returns the password for the created user.""" - return self.relation.data[self.relation.app].get("password") - - @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 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") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - return self.relation.data[self.relation.app].get("username") - - @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(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(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) - - -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 — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() - - 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) - self.charm = charm - self.database = database_name - 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.relations_aliases = relations_aliases - 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 - ) - - # 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 _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. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].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] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - 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 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) - - 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") - - @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]) diff --git a/charms/placement-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/placement-k8s/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3f..00000000 --- a/charms/placement-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -Also provide additional parameters to the charm object: - - service - - internal_url - - public_url - - admin_url - - region - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - service = "my-service" - internal_url = "http://internal-url" - public_url = "http://public-url" - admin_url = "http://admin-url" - region = "region" - ) - self.framework.observe( - self.identity_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# 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 - - -logger = logging.getLogger(__name__) - - -class IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService relation.""" - return self.framework.model.get_relation(self.relation_name) - - def get_remote_app_data(self, key: str) -> str: - """Return the value for the given key from remote app data.""" - data = self._identity_service_rel.data[self._identity_service_rel.app] - return data.get(key) - - @property - def api_version(self) -> str: - """Return the api_version.""" - return self.get_remote_app_data('api-version') - - @property - def auth_host(self) -> str: - """Return the auth_host.""" - return self.get_remote_app_data('auth-host') - - @property - def auth_port(self) -> str: - """Return the auth_port.""" - return self.get_remote_app_data('auth-port') - - @property - def auth_protocol(self) -> str: - """Return the auth_protocol.""" - return self.get_remote_app_data('auth-protocol') - - @property - def internal_host(self) -> str: - """Return the internal_host.""" - return self.get_remote_app_data('internal-host') - - @property - def internal_port(self) -> str: - """Return the internal_port.""" - return self.get_remote_app_data('internal-port') - - @property - def internal_protocol(self) -> str: - """Return the internal_protocol.""" - return self.get_remote_app_data('internal-protocol') - - @property - def admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("password") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("username") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_credentials(self, relation_name: int, - relation_id: str, - api_version: str, - auth_host: str, - auth_port: str, - auth_protocol: str, - internal_host: str, - internal_port: str, - internal_protocol: str, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_rel.data[self.charm.app] - app_data["api-version"] = api_version - app_data["auth-host"] = auth_host - app_data["auth-port"] = str(auth_port) - app_data["auth-protocol"] = auth_protocol - app_data["internal-host"] = internal_host - app_data["internal-port"] = str(internal_port) - app_data["internal-protocol"] = internal_protocol - app_data["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/charms/placement-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/placement-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df2409..00000000 --- a/charms/placement-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - username - - vhost - -Two events are also available to respond to: - - connected - - ready - - goneaway - -A basic example showing the usage of this relation follows: - -``` -from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/placement-k8s/lib/charms/traefik_k8s/v2/ingress.py b/charms/placement-k8s/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8ab..00000000 --- a/charms/placement-k8s/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/charms/placement-k8s/osci.yaml b/charms/placement-k8s/osci.yaml deleted file mode 100644 index 60172b1c..00000000 --- a/charms/placement-k8s/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: placement-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/charms/placement-k8s/pyproject.toml b/charms/placement-k8s/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/charms/placement-k8s/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/charms/placement-k8s/rename.sh b/charms/placement-k8s/rename.sh deleted file mode 100755 index d0c35c97..00000000 --- a/charms/placement-k8s/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/charms/placement-k8s/requirements.txt b/charms/placement-k8s/requirements.txt index e988c61d..64399ba9 100644 --- a/charms/placement-k8s/requirements.txt +++ b/charms/placement-k8s/requirements.txt @@ -12,4 +12,5 @@ lightkube lightkube-models ops -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam +# From ops_sunbeam +tenacity diff --git a/charms/placement-k8s/src/templates/parts/section-database b/charms/placement-k8s/src/templates/parts/section-database deleted file mode 100644 index eb52f65e..00000000 --- a/charms/placement-k8s/src/templates/parts/section-database +++ /dev/null @@ -1,7 +0,0 @@ -[database] -{% if database.connection -%} -connection = {{ database.connection }} -{% else -%} -connection = sqlite:////var/lib/cinder/cinder.db -{% endif -%} -connection_recycle_time = 200 diff --git a/charms/placement-k8s/src/templates/parts/section-federation b/charms/placement-k8s/src/templates/parts/section-federation deleted file mode 100644 index 65ee99ed..00000000 --- a/charms/placement-k8s/src/templates/parts/section-federation +++ /dev/null @@ -1,10 +0,0 @@ -{% if trusted_dashboards %} -[federation] -{% for dashboard_url in trusted_dashboards -%} -trusted_dashboard = {{ dashboard_url }} -{% endfor -%} -{% endif %} -{% for sp in fid_sps -%} -[{{ sp['protocol-name'] }}] -remote_id_attribute = {{ sp['remote-id-attribute'] }} -{% endfor -%} diff --git a/charms/placement-k8s/src/templates/parts/section-identity b/charms/placement-k8s/src/templates/parts/section-identity deleted file mode 100644 index cbb1d069..00000000 --- a/charms/placement-k8s/src/templates/parts/section-identity +++ /dev/null @@ -1,24 +0,0 @@ -[keystone_authtoken] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True diff --git a/charms/placement-k8s/src/templates/parts/section-middleware b/charms/placement-k8s/src/templates/parts/section-middleware deleted file mode 100644 index e65f1d98..00000000 --- a/charms/placement-k8s/src/templates/parts/section-middleware +++ /dev/null @@ -1,6 +0,0 @@ -{% for section in sections -%} -[{{section}}] -{% for key, value in sections[section].items() -%} -{{ key }} = {{ value }} -{% endfor %} -{%- endfor %} diff --git a/charms/placement-k8s/src/templates/parts/section-service-user b/charms/placement-k8s/src/templates/parts/section-service-user deleted file mode 100644 index 65103693..00000000 --- a/charms/placement-k8s/src/templates/parts/section-service-user +++ /dev/null @@ -1,17 +0,0 @@ -{% if identity_service.service_domain_id -%} -[service_user] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -send_service_user_token = true -auth_type = password -project_domain_id = {{ identity_service.service_domain_id }} -user_domain_id = {{ identity_service.service_domain_id }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -{% endif -%} diff --git a/charms/placement-k8s/src/templates/parts/section-signing b/charms/placement-k8s/src/templates/parts/section-signing deleted file mode 100644 index cb7d69ae..00000000 --- a/charms/placement-k8s/src/templates/parts/section-signing +++ /dev/null @@ -1,15 +0,0 @@ -{% if enable_signing -%} -[signing] -{% if certfile -%} -certfile = {{ certfile }} -{% endif -%} -{% if keyfile -%} -keyfile = {{ keyfile }} -{% endif -%} -{% if ca_certs -%} -ca_certs = {{ ca_certs }} -{% endif -%} -{% if ca_key -%} -ca_key = {{ ca_key }} -{% endif -%} -{% endif -%} \ No newline at end of file diff --git a/charms/placement-k8s/src/templates/placement.conf b/charms/placement-k8s/src/templates/placement.conf index c8f3673c..771165cf 100644 --- a/charms/placement-k8s/src/templates/placement.conf +++ b/charms/placement-k8s/src/templates/placement.conf @@ -5,11 +5,7 @@ debug = {{ options.debug }} auth_strategy = keystone [placement_database] -{% if database.connection -%} -connection = {{ database.connection }} -{% else -%} -connection = sqlite:////var/lib/placement/placement.db -{% endif -%} +{% include "parts/database-connection" %} {% include "parts/section-identity" %} diff --git a/charms/placement-k8s/test-requirements.txt b/charms/placement-k8s/test-requirements.txt deleted file mode 100644 index a9b0d698..00000000 --- a/charms/placement-k8s/test-requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -coverage -mock -flake8 -stestr -git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza -git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack -git+https://opendev.org/openstack/tempest.git#egg=tempest -ops -# Subunit 1.4.3+ requires extras -extras diff --git a/charms/placement-k8s/tests/unit/test_placement_charm.py b/charms/placement-k8s/tests/unit/test_placement_charm.py index 21ed982f..dc5da6b7 100644 --- a/charms/placement-k8s/tests/unit/test_placement_charm.py +++ b/charms/placement-k8s/tests/unit/test_placement_charm.py @@ -18,9 +18,8 @@ import textwrap -import ops_sunbeam.test_utils as test_utils - import charm +import ops_sunbeam.test_utils as test_utils class _PlacementOperatorCharm(charm.PlacementOperatorCharm): @@ -115,6 +114,8 @@ class TestPlacementOperatorCharm(test_utils.CharmTestCase): [placement_database] connection = mysql+pymysql://foo:hardpassword@10.0.0.10/placement_api + + [keystone_authtoken] auth_url = http://keystone.internal:5000 interface = internal diff --git a/charms/placement-k8s/tox.ini b/charms/placement-k8s/tox.ini deleted file mode 100644 index 0ab1edc3..00000000 --- a/charms/placement-k8s/tox.ini +++ /dev/null @@ -1,161 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - lib/* - -[flake8] -ignore=E226,W504 diff --git a/common.sh b/common.sh new file mode 100644 index 00000000..eb5e5d76 --- /dev/null +++ b/common.sh @@ -0,0 +1,418 @@ +#!/bin/bash + +# All libraries required by sunbeam charms are centrally +# maintained in libs folder. The libraries created by +# sunbeam charms are placed in libs/internal and the +# libraries provided by external charms are maintained +# in libs/external. +# All generic template parts are maintained in +# templates/parts folder. +# +# This script provides functions for each sunbeam charms +# all the common files that should be copied to charm +# for building the charm and function testing. + + +NULL_ARRAY=() + +# Internal libs for component. If libs are repeated, reuse the existing component +INTERNAL_CEILOMETER_LIBS=( + "keystone_k8s" + "ceilometer_k8s" + "gnocchi_k8s" +) + +INTERNAL_CINDER_LIBS=( + "keystone_k8s" + "cinder_k8s" +) + +INTERNAL_CINDER_CEPH_LIBS=( + "keystone_k8s" + "cinder_k8s" + "cinder_ceph_k8s" +) + +INTERNAL_DESIGNATE_LIBS=( + "keystone_k8s" + "designate_bind_k8s" +) + +INTERNAL_DESIGNATE_BIND_LIBS=( + "designate_bind_k8s" +) + +INTERNAL_GNOCCHI_LIBS=( + "keystone_k8s" + "gnocchi_k8s" +) + +INTERNAL_KEYSTONE_LIBS=( + "keystone_k8s" +) + +INTERNAL_NEUTRON_LIBS=( + "keystone_k8s" + "ovn_central_k8s" +) + +INTERNAL_NOVA_LIBS=( + "keystone_k8s" + "sunbeam_nova_compute_operator" +) + +INTERNAL_OPENSTACK_HYPERVISOR_LIBS=( + "keystone_k8s" + "ovn_central_k8s" + "cinder_ceph_k8s" + "ceilometer_k8s" +) + +INTERNAL_OVN_CENTRAL_LIBS=( + "ovn_central_k8s" +) + +# External libs for component. If libs are repeated, reuse the existing component +EXTERNAL_AODH_LIBS=( + "data_platform_libs" + "rabbitmq_k8s" + "traefik_k8s" +) + +EXTERNAL_BARBICAN_LIBS=( + "data_platform_libs" + "rabbitmq_k8s" + "traefik_k8s" + "vault_k8s" +) + +EXTERNAL_CEILOMETER_LIBS=( + "rabbitmq_k8s" +) + +EXTERNAL_DESIGNATE_BIND_LIBS=( + "observability_libs" +) + +EXTERNAL_HEAT_LIBS=( + "data_platform_libs" + "rabbitmq_k8s" + "traefik_route_k8s" +) + +EXTERNAL_NEUTRON_LIBS=( + "data_platform_libs" + "rabbitmq_k8s" + "traefik_k8s" + "tls_certificates_interface" +) + +EXTERNAL_OCTAVIA_LIBS=( + "data_platform_libs" + "traefik_k8s" + "tls_certificates_interface" +) + +EXTERNAL_OPENSTACK_EXPORTER_LIBS=( + "grafana_k8s" + "prometheus_k8s" + "tls_certificates_interface" +) + +EXTERNAL_OPENSTACK_HYPERVISOR_LIBS=( + "data_platform_libs" + "grafana_agent" + "observability_libs" + "operator_libs_linux" + "rabbitmq_k8s" + "traefik_k8s" + "tls_certificates_interface" +) + +EXTERNAL_OVN_CENTRAL_LIBS=( + "tls_certificates_interface" +) + +EXTERNAL_OVN_RELAY_LIBS=( + "tls_certificates_interface" + "observability_libs" +) + +# Config template parts for each component. +CONFIG_TEMPLATES_AODH=( + "section-database" + "database-connection" + "section-identity" + "identity-data" + "section-oslo-messaging-rabbit" + "section-service-credentials" +) + +CONFIG_TEMPLATES_BARBICAN=( + "section-identity" + "identity-data" + "section-oslo-messaging-rabbit" + "section-service-user" +) + +CONFIG_TEMPLATES_CEILOMETER=( + "identity-data-id-creds" + "section-oslo-messaging-rabbit" + "section-service-credentials-from-identity-service" + "section-service-user-from-identity-credentials" +) + +CONFIG_TEMPLATES_CINDER=( + "section-database" + "database-connection" + "section-identity" + "identity-data" + "section-oslo-messaging-rabbit" + "section-service-user" +) + +CONFIG_TEMPLATES_CINDER_CEPH=( + "section-oslo-messaging-rabbit" + "section-oslo-notifications" +) + +CONFIG_TEMPLATES_DESIGNATE=( + "database-connection" + "section-identity" + "identity-data" + "section-oslo-messaging-rabbit" + "section-service-user" +) + +CONFIG_TEMPLATES_GLANCE=( + "section-database" + "database-connection" + "section-identity" + "identity-data" + "section-oslo-messaging-rabbit" + "section-oslo-notifications" + "section-service-user" +) + +CONFIG_TEMPLATES_GNOCCHI=( + "database-connection" + "section-identity" + "identity-data" +) + +CONFIG_TEMPLATES_HEAT=( + "section-database" + "database-connection" + "section-identity" + "identity-data" + "section-oslo-messaging-rabbit" +) + +CONFIG_TEMPLATES_KEYSTONE=( + "section-database" + "database-connection" + "section-federation" + "section-middleware" + "section-oslo-cache" + "section-oslo-messaging-rabbit" + "section-oslo-middleware" + "section-oslo-notifications" + "section-signing" +) + +CONFIG_TEMPLATES_MAGNUM=( + "section-identity" + "identity-data" + "section-oslo-messaging-rabbit" + "section-service-user" + "section-trust" +) + +CONFIG_TEMPLATES_NEUTRON=( + "section-database" + "database-connection" + "section-identity" + "identity-data" + "section-oslo-messaging-rabbit" + "section-service-user" +) + +CONFIG_TEMPLATES_NOVA=${CONFIG_TEMPLATES_NEUTRON[@]} + +CONFIG_TEMPLATES_OCTAVIA=( + "section-database" + "database-connection" + "section-identity" + "identity-data" +) + +CONFIG_TEMPLATES_PLACEMENT=( + "database-connection" + "section-identity" + "identity-data" + "section-service-user" +) + +declare -A INTERNAL_LIBS=( + [aodh-k8s]=${INTERNAL_KEYSTONE_LIBS[@]} + [barbican-k8s]=${INTERNAL_KEYSTONE_LIBS[@]} + [ceilometer-k8s]=${INTERNAL_CEILOMETER_LIBS[@]} + [cinder-k8s]=${INTERNAL_CINDER_LIBS[@]} + [cinder-ceph-k8s]=${INTERNAL_CINDER_CEPH_LIBS[@]} + [designate-k8s]=${INTERNAL_DESIGNATE_LIBS[@]} + [designate-bind-k8s]=${INTERNAL_DESIGNATE_BIND_LIBS[@]} + [glance-k8s]=${INTERNAL_KEYSTONE_LIBS[@]} + [gnocchi-k8s]=${INTERNAL_GNOCCHI_LIBS[@]} + [heat-k8s]=${INTERNAL_KEYSTONE_LIBS[@]} + [horizon-k8s]=${INTERNAL_KEYSTONE_LIBS[@]} + [keystone-k8s]=${INTERNAL_KEYSTONE_LIBS[@]} + [keystone-ldap-k8s]=${INTERNAL_KEYSTONE_LIBS[@]} + [magnum-k8s]=${INTERNAL_KEYSTONE_LIBS[@]} + [neutron-k8s]=${INTERNAL_NEUTRON_LIBS[@]} + [nova-k8s]=${INTERNAL_NOVA_LIBS[@]} + [octavia-k8s]=${INTERNAL_NEUTRON_LIBS[@]} + [openstack-exporter-k8s]=${INTERNAL_KEYSTONE_LIBS[@]} + [openstack-hypervisor]=${INTERNAL_OPENSTACK_HYPERVISOR_LIBS[@]} + [ovn-central-k8s]=${INTERNAL_OVN_CENTRAL_LIBS[@]} + [ovn-relay-k8s]=${INTERNAL_OVN_CENTRAL_LIBS[@]} + [placement-k8s]=${INTERNAL_KEYSTONE_LIBS[@]} +) + +declare -A EXTERNAL_LIBS=( + [aodh-k8s]=${EXTERNAL_AODH_LIBS[@]} + [barbican-k8s]=${EXTERNAL_BARBICAN_LIBS[@]} + [ceilometer-k8s]=${EXTERNAL_CEILOMETER_LIBS[@]} + [cinder-k8s]=${EXTERNAL_AODH_LIBS[@]} + [cinder-ceph-k8s]=${EXTERNAL_AODH_LIBS[@]} + [designate-k8s]=${EXTERNAL_AODH_LIBS[@]} + [designate-bind-k8s]=${EXTERNAL_DESIGNATE_BIND_LIBS[@]} + [glance-k8s]=${EXTERNAL_AODH_LIBS[@]} + [gnocchi-k8s]=${EXTERNAL_AODH_LIBS[@]} + [heat-k8s]=${EXTERNAL_HEAT_LIBS[@]} + [horizon-k8s]=${EXTERNAL_AODH_LIBS[@]} + [keystone-k8s]=${EXTERNAL_AODH_LIBS[@]} + [keystone-ldap-k8s]=${NULL_ARRAY[@]} + [magnum-k8s]=${EXTERNAL_AODH_LIBS[@]} + [neutron-k8s]=${EXTERNAL_NEUTRON_LIBS[@]} + [nova-k8s]=${EXTERNAL_AODH_LIBS[@]} + [octavia-k8s]=${EXTERNAL_OCTAVIA_LIBS[@]} + [openstack-exporter-k8s]=${EXTERNAL_OPENSTACK_EXPORTER_LIBS[@]} + [openstack-hypervisor]=${EXTERNAL_OPENSTACK_HYPERVISOR_LIBS[@]} + [ovn-central-k8s]=${EXTERNAL_OVN_CENTRAL_LIBS[@]} + [ovn-relay-k8s]=${EXTERNAL_OVN_RELAY_LIBS[@]} + [placement-k8s]=${EXTERNAL_AODH_LIBS[@]} +) + +declare -A CONFIG_TEMPLATES=( + [aodh-k8s]=${CONFIG_TEMPLATES_AODH[@]} + [barbican-k8s]=${CONFIG_TEMPLATES_BARBICAN[@]} + [ceilometer-k8s]=${CONFIG_TEMPLATES_CEILOMETER[@]} + [cinder-k8s]=${CONFIG_TEMPLATES_CINDER[@]} + [cinder-ceph-k8s]=${CONFIG_TEMPLATES_CINDER_CEPH[@]} + [designate-k8s]=${CONFIG_TEMPLATES_DESIGNATE[@]} + [designate-bind-k8s]=${NULL_ARRAY[@]} + [glance-k8s]=${CONFIG_TEMPLATES_GLANCE[@]} + [gnocchi-k8s]=${CONFIG_TEMPLATES_GNOCCHI[@]} + [heat-k8s]=${CONFIG_TEMPLATES_HEAT[@]} + [horizon-k8s]=${NULL_ARRAY[@]} + [keystone-k8s]=${CONFIG_TEMPLATES_KEYSTONE[@]} + [keystone-ldap-k8s]=${NULL_ARRAY[@]} + [magnum-k8s]=${CONFIG_TEMPLATES_MAGNUM[@]} + [neutron-k8s]=${CONFIG_TEMPLATES_NEUTRON[@]} + [nova-k8s]=${CONFIG_TEMPLATES_NOVA[@]} + [octavia-k8s]=${CONFIG_TEMPLATES_OCTAVIA[@]} + [openstack-exporter-k8s]=${NULL_ARRAY[@]} + [openstack-hypervisor]=${NULL_ARRAY[@]} + [ovn-central-k8s]=${NULL_ARRAY[@]} + [ovn-relay-k8s]=${NULL_ARRAY[@]} + [placement-k8s]=${CONFIG_TEMPLATES_PLACEMENT[@]} +) + + +function copy_ops_sunbeam { + cp -rf ../../ops-sunbeam/ops_sunbeam lib/ +} + +function copy_internal_libs { + internal_libs_=${INTERNAL_LIBS[$1]} + echo "copy_internal_libs for $1:" + for lib in ${internal_libs_[@]}; do + echo "Copying $lib" + cp -rf ../../libs/internal/lib/charms/$lib lib/charms/ + done +} + +function copy_external_libs { + echo "copy_external_libs for $1:" + external_libs_=${EXTERNAL_LIBS[$1]} + for lib in ${external_libs_[@]}; do + echo "Copying $lib" + cp -rf ../../libs/external/lib/charms/$lib lib/charms/ + done +} + +function copy_config_templates { + echo "copy_config_templates for $1:" + config_templates_=${CONFIG_TEMPLATES[$1]} + for part in ${config_templates_[@]}; do + echo "Copying $part" + cp -rf ../../templates/parts/$part src/templates/parts/ + done +} + +function copy_juju_ignore { + cp ../../.jujuignore . +} + +function copy_stestr_conf { + cp ../../.stestr.conf . +} + +function remove_libs { + rm -rf lib +} + +function remove_templates_parts_dir { + rm -rf src/templates/parts +} + +function remove_juju_ignore { + rm .jujuignore +} + +function remove_stestr_conf { + rm .stestr.conf +} + +function push_common_files { + if [[ $# != 1 ]]; + then + echo "push_common_files: Expected one argument" + exit 1 + fi + + pushd charms/$1 + + mkdir -p lib/charms + mkdir -p src/templates/parts + + copy_ops_sunbeam + copy_internal_libs $1 + copy_external_libs $1 + copy_config_templates $1 + copy_stestr_conf + copy_juju_ignore + + popd +} + +function pop_common_files { + pushd charms/$1 + + remove_libs + remove_templates_parts_dir + remove_stestr_conf + remove_juju_ignore + + popd +} diff --git a/fetch_libs.sh b/fetch_libs.sh new file mode 100755 index 00000000..8521bcc7 --- /dev/null +++ b/fetch_libs.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +pushd libs/external + +echo "INFO: Fetching libs from charmhub." +charmcraft fetch-lib charms.data_platform_libs.v0.database_requires +charmcraft fetch-lib charms.grafana_k8s.v0.grafana_auth +charmcraft fetch-lib charms.observability_libs.v1.kubernetes_service_patch +charmcraft fetch-lib charms.operator_libs_linux.v2.snap +charmcraft fetch-lib charms.prometheus_k8s.v0.prometheus_scrape +charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq +charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates +charmcraft fetch-lib charms.traefik_k8s.v2.ingress +charmcraft fetch-lib charms.traefik_route_k8s.v0.traefik_route +charmcraft fetch-lib charms.vault_k8s.v0.vault_kv + +popd diff --git a/charms/cinder-ceph-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/libs/external/lib/charms/data_platform_libs/v0/database_requires.py similarity index 100% rename from charms/cinder-ceph-k8s/lib/charms/data_platform_libs/v0/database_requires.py rename to libs/external/lib/charms/data_platform_libs/v0/database_requires.py diff --git a/charms/openstack-hypervisor/lib/charms/grafana_agent/v0/cos_agent.py b/libs/external/lib/charms/grafana_agent/v0/cos_agent.py similarity index 100% rename from charms/openstack-hypervisor/lib/charms/grafana_agent/v0/cos_agent.py rename to libs/external/lib/charms/grafana_agent/v0/cos_agent.py diff --git a/charms/openstack-exporter-k8s/lib/charms/grafana_k8s/v0/grafana_dashboard.py b/libs/external/lib/charms/grafana_k8s/v0/grafana_dashboard.py similarity index 100% rename from charms/openstack-exporter-k8s/lib/charms/grafana_k8s/v0/grafana_dashboard.py rename to libs/external/lib/charms/grafana_k8s/v0/grafana_dashboard.py diff --git a/charms/designate-bind-k8s/lib/charms/observability_libs/v1/kubernetes_service_patch.py b/libs/external/lib/charms/observability_libs/v1/kubernetes_service_patch.py similarity index 100% rename from charms/designate-bind-k8s/lib/charms/observability_libs/v1/kubernetes_service_patch.py rename to libs/external/lib/charms/observability_libs/v1/kubernetes_service_patch.py diff --git a/charms/openstack-hypervisor/lib/charms/operator_libs_linux/v2/snap.py b/libs/external/lib/charms/operator_libs_linux/v2/snap.py similarity index 100% rename from charms/openstack-hypervisor/lib/charms/operator_libs_linux/v2/snap.py rename to libs/external/lib/charms/operator_libs_linux/v2/snap.py diff --git a/charms/openstack-exporter-k8s/lib/charms/prometheus_k8s/v0/prometheus_scrape.py b/libs/external/lib/charms/prometheus_k8s/v0/prometheus_scrape.py similarity index 100% rename from charms/openstack-exporter-k8s/lib/charms/prometheus_k8s/v0/prometheus_scrape.py rename to libs/external/lib/charms/prometheus_k8s/v0/prometheus_scrape.py diff --git a/charms/aodh-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/libs/external/lib/charms/rabbitmq_k8s/v0/rabbitmq.py similarity index 100% rename from charms/aodh-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py rename to libs/external/lib/charms/rabbitmq_k8s/v0/rabbitmq.py diff --git a/charms/neutron-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py b/libs/external/lib/charms/tls_certificates_interface/v1/tls_certificates.py similarity index 100% rename from charms/neutron-k8s/lib/charms/tls_certificates_interface/v1/tls_certificates.py rename to libs/external/lib/charms/tls_certificates_interface/v1/tls_certificates.py diff --git a/charms/aodh-k8s/lib/charms/traefik_k8s/v2/ingress.py b/libs/external/lib/charms/traefik_k8s/v2/ingress.py similarity index 100% rename from charms/aodh-k8s/lib/charms/traefik_k8s/v2/ingress.py rename to libs/external/lib/charms/traefik_k8s/v2/ingress.py diff --git a/charms/heat-k8s/lib/charms/traefik_route_k8s/v0/traefik_route.py b/libs/external/lib/charms/traefik_route_k8s/v0/traefik_route.py similarity index 100% rename from charms/heat-k8s/lib/charms/traefik_route_k8s/v0/traefik_route.py rename to libs/external/lib/charms/traefik_route_k8s/v0/traefik_route.py diff --git a/charms/barbican-k8s/lib/charms/vault_k8s/v0/vault_kv.py b/libs/external/lib/charms/vault_k8s/v0/vault_kv.py similarity index 100% rename from charms/barbican-k8s/lib/charms/vault_k8s/v0/vault_kv.py rename to libs/external/lib/charms/vault_k8s/v0/vault_kv.py diff --git a/charms/ceilometer-k8s/lib/charms/ceilometer_k8s/v0/ceilometer_service.py b/libs/internal/lib/charms/ceilometer_k8s/v0/ceilometer_service.py similarity index 100% rename from charms/ceilometer-k8s/lib/charms/ceilometer_k8s/v0/ceilometer_service.py rename to libs/internal/lib/charms/ceilometer_k8s/v0/ceilometer_service.py diff --git a/charms/cinder-ceph-k8s/lib/charms/cinder_ceph_k8s/v0/ceph_access.py b/libs/internal/lib/charms/cinder_ceph_k8s/v0/ceph_access.py similarity index 100% rename from charms/cinder-ceph-k8s/lib/charms/cinder_ceph_k8s/v0/ceph_access.py rename to libs/internal/lib/charms/cinder_ceph_k8s/v0/ceph_access.py diff --git a/charms/cinder-ceph-k8s/lib/charms/cinder_k8s/v0/storage_backend.py b/libs/internal/lib/charms/cinder_k8s/v0/storage_backend.py similarity index 100% rename from charms/cinder-ceph-k8s/lib/charms/cinder_k8s/v0/storage_backend.py rename to libs/internal/lib/charms/cinder_k8s/v0/storage_backend.py diff --git a/charms/designate-bind-k8s/lib/charms/designate_bind_k8s/v0/bind_rndc.py b/libs/internal/lib/charms/designate_bind_k8s/v0/bind_rndc.py similarity index 100% rename from charms/designate-bind-k8s/lib/charms/designate_bind_k8s/v0/bind_rndc.py rename to libs/internal/lib/charms/designate_bind_k8s/v0/bind_rndc.py diff --git a/charms/ceilometer-k8s/lib/charms/gnocchi_k8s/v0/gnocchi_service.py b/libs/internal/lib/charms/gnocchi_k8s/v0/gnocchi_service.py similarity index 100% rename from charms/ceilometer-k8s/lib/charms/gnocchi_k8s/v0/gnocchi_service.py rename to libs/internal/lib/charms/gnocchi_k8s/v0/gnocchi_service.py diff --git a/charms/gnocchi-k8s/lib/charms/gnocchi_k8s/v0/metric_service.py b/libs/internal/lib/charms/gnocchi_k8s/v0/metric_service.py similarity index 100% rename from charms/gnocchi-k8s/lib/charms/gnocchi_k8s/v0/metric_service.py rename to libs/internal/lib/charms/gnocchi_k8s/v0/metric_service.py diff --git a/charms/keystone-k8s/lib/charms/keystone_k8s/v0/cloud_credentials.py b/libs/internal/lib/charms/keystone_k8s/v0/cloud_credentials.py similarity index 100% rename from charms/keystone-k8s/lib/charms/keystone_k8s/v0/cloud_credentials.py rename to libs/internal/lib/charms/keystone_k8s/v0/cloud_credentials.py diff --git a/charms/keystone-k8s/lib/charms/keystone_k8s/v0/domain_config.py b/libs/internal/lib/charms/keystone_k8s/v0/domain_config.py similarity index 100% rename from charms/keystone-k8s/lib/charms/keystone_k8s/v0/domain_config.py rename to libs/internal/lib/charms/keystone_k8s/v0/domain_config.py diff --git a/charms/ceilometer-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py b/libs/internal/lib/charms/keystone_k8s/v0/identity_credentials.py similarity index 100% rename from charms/ceilometer-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py rename to libs/internal/lib/charms/keystone_k8s/v0/identity_credentials.py diff --git a/charms/heat-k8s/lib/charms/keystone_k8s/v0/identity_resource.py b/libs/internal/lib/charms/keystone_k8s/v0/identity_resource.py similarity index 100% rename from charms/heat-k8s/lib/charms/keystone_k8s/v0/identity_resource.py rename to libs/internal/lib/charms/keystone_k8s/v0/identity_resource.py diff --git a/charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_service.py b/libs/internal/lib/charms/keystone_k8s/v0/identity_service.py similarity index 100% rename from charms/keystone-k8s/lib/charms/keystone_k8s/v0/identity_service.py rename to libs/internal/lib/charms/keystone_k8s/v0/identity_service.py diff --git a/charms/keystone-k8s/lib/charms/keystone_k8s/v1/cloud_credentials.py b/libs/internal/lib/charms/keystone_k8s/v1/cloud_credentials.py similarity index 100% rename from charms/keystone-k8s/lib/charms/keystone_k8s/v1/cloud_credentials.py rename to libs/internal/lib/charms/keystone_k8s/v1/cloud_credentials.py diff --git a/charms/keystone-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/libs/internal/lib/charms/keystone_k8s/v1/identity_service.py similarity index 100% rename from charms/keystone-k8s/lib/charms/keystone_k8s/v1/identity_service.py rename to libs/internal/lib/charms/keystone_k8s/v1/identity_service.py diff --git a/charms/neutron-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py b/libs/internal/lib/charms/ovn_central_k8s/v0/ovsdb.py similarity index 100% rename from charms/neutron-k8s/lib/charms/ovn_central_k8s/v0/ovsdb.py rename to libs/internal/lib/charms/ovn_central_k8s/v0/ovsdb.py diff --git a/charms/nova-k8s/lib/charms/sunbeam_nova_compute_operator/v0/cloud_compute.py b/libs/internal/lib/charms/sunbeam_nova_compute_operator/v0/cloud_compute.py similarity index 100% rename from charms/nova-k8s/lib/charms/sunbeam_nova_compute_operator/v0/cloud_compute.py rename to libs/internal/lib/charms/sunbeam_nova_compute_operator/v0/cloud_compute.py diff --git a/ops-sunbeam/pyproject.toml b/ops-sunbeam/pyproject.toml deleted file mode 100644 index 2896bc05..00000000 --- a/ops-sunbeam/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/ops-sunbeam/test-requirements.txt b/ops-sunbeam/test-requirements.txt deleted file mode 100644 index b196466f..00000000 --- a/ops-sunbeam/test-requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -coverage -mock -stestr -requests -pytest -ops-scenario>=4.0 diff --git a/ops-sunbeam/tox.ini b/ops-sunbeam/tox.ini deleted file mode 100644 index 2cdf6310..00000000 --- a/ops-sunbeam/tox.ini +++ /dev/null @@ -1,135 +0,0 @@ -# Operator charm helper: tox.ini - -[tox] -skipsdist = True -envlist = lint, py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/ops_sunbeam -tst_path = {toxinidir}/tests/unit_tests/ -scenario_tst_path = {toxinidir}/tests/scenario_tests/ -tst_lib_path = {toxinidir}/tests/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -cookie_cutter_path = {toxinidir}/shared_code/sunbeam_charm/\{\{cookiecutter.service_name\}\} -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -install_command = - pip install {opts} {packages} -commands = - stestr run --slowest {posargs} - pytest -v --tb native {[vars]scenario_tst_path} --log-cli-level=INFO -allowlist_externals = - git - charmcraft - fetch-libs.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]tst_lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]tst_lib_path} - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:cookie] -basepython = python3 -deps = -r{toxinidir}/cookie-requirements.txt -commands = /bin/true - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]tst_lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]tst_lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]tst_lib_path} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:scenario] -description = Scenario tests -deps = - -r{toxinidir}/requirements.txt - -r{toxinidir}/test-requirements.txt -commands = - pytest -v --tb native {[vars]scenario_tst_path} --log-cli-level=INFO - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - unit_tests/* - -[testenv:venv] -basepython = python3 -commands = {posargs} - -[flake8] -ignore = E226,E402,ANN101,ANN003,W504 diff --git a/playbooks/charm/build.yaml b/playbooks/charm/build.yaml new file mode 100644 index 00000000..e794a3ad --- /dev/null +++ b/playbooks/charm/build.yaml @@ -0,0 +1,6 @@ +- hosts: all + roles: + - ensure-tox + - role: charm-build + vars: + charm_build_name: "{{ charm }}" diff --git a/playbooks/charm/publish.yaml b/playbooks/charm/publish.yaml new file mode 100644 index 00000000..547b9240 --- /dev/null +++ b/playbooks/charm/publish.yaml @@ -0,0 +1,3 @@ +- hosts: all + roles: + - charm-publish diff --git a/playbooks/collect-run-data.yaml b/playbooks/collect-run-data.yaml new file mode 100644 index 00000000..cf81022a --- /dev/null +++ b/playbooks/collect-run-data.yaml @@ -0,0 +1,3 @@ +- hosts: all + roles: + - collect-run-data diff --git a/playbooks/zaza-func-test.yaml b/playbooks/zaza-func-test.yaml new file mode 100644 index 00000000..41e5c315 --- /dev/null +++ b/playbooks/zaza-func-test.yaml @@ -0,0 +1,6 @@ +- hosts: all + roles: + - ensure-tox + - use-docker-mirror + - microk8s-cloud + - zaza-func-test diff --git a/charms/aodh-k8s/pyproject.toml b/pyproject.toml similarity index 100% rename from charms/aodh-k8s/pyproject.toml rename to pyproject.toml diff --git a/render_bundles.py b/render_bundles.py new file mode 100644 index 00000000..4a10bddf --- /dev/null +++ b/render_bundles.py @@ -0,0 +1,54 @@ +#!/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. + + +"""Render all smoke bundles. + +Renders smoke bundles with context of locally built charms. +Prepares the context with assumption the charm is locally +built if corresponding *.charm exists in current folder. + +Assumption: All build charms will be in sunbeam-charms folder. +""" + +import glob +from pathlib import ( + Path, +) + +from jinja2 import ( + Environment, + FileSystemLoader, +) + +test_directories = [ dir_.name for dir_ in list(Path("tests").glob('*')) ] +built_charms = glob.glob("*.charm") +context = { + charm.rstrip(".charm").replace("-", "_"): True for charm in built_charms +} +print(f"Using context: {context}") + +for test_dir in test_directories: + bundle_dir = f"tests/{test_dir}" + template_loader = Environment(loader=FileSystemLoader(bundle_dir)) + bundle_template = template_loader.get_template("smoke.yaml.j2") + smoke_file = Path(f"{bundle_dir}/bundles/smoke.yaml") + smoke_file.parent.mkdir(parents=True, exist_ok=True) + with smoke_file.open("w", encoding="utf-8") as content: + content.write(bundle_template.render(context)) + print(f"Rendered smoke bundle: {smoke_file}") + with smoke_file.open("r", encoding="utf-8") as content: + print(content.read()) diff --git a/roles/charm-build/tasks/main.yaml b/roles/charm-build/tasks/main.yaml new file mode 100644 index 00000000..f4e3bd7e --- /dev/null +++ b/roles/charm-build/tasks/main.yaml @@ -0,0 +1,69 @@ +- name: lxd apt packages are not present + apt: + name: + - lxd + - lxd-client + state: absent + purge: true + become: true + +- name: snapd is installed + apt: + name: snapd + become: true + +- name: lxd snap is installed + snap: + name: lxd + channel: latest/stable + become: true + +- name: lxd is initialised + command: lxd init --auto + become: true + +- name: current user is in lxd group + user: + name: "{{ ansible_user }}" + groups: lxd + append: true + become: true + +- name: reset ssh connection to apply permissions from new group + meta: reset_connection + +- name: charmcraft is installed + snap: + name: charmcraft + channel: "{{ charmcraft_channel | default('latest/stable') }}" + classic: true + become: true + +- name: charm is packed + command: + cmd: "{{ tox_executable }} -e build -- {{ charm_build_name }}" + chdir: "{{ zuul.project.src_dir }}" + register: res + retries: 3 + delay: 30 + until: > + "Charm packed ok" in res.stdout + failed_when: '"Failed instance creation" in res.stdout' + +- name: built charm is available in the zuul log root for auto artifact upload + fetch: + src: "{{ zuul.project.src_dir }}/charms/{{ charm_build_name }}/{{ charm_build_name }}.charm" + dest: "{{ zuul.executor.log_root }}/" + flat: true + become: true + +- name: Upload artifacts + zuul_return: + data: + zuul: + artifacts: + - name: charm + url: "{{ charm_build_name }}.charm" + metadata: + type: charm + name: "{{ charm_build_name }}" diff --git a/roles/charm-publish/defaults/main.yaml b/roles/charm-publish/defaults/main.yaml new file mode 100644 index 00000000..a9306832 --- /dev/null +++ b/roles/charm-publish/defaults/main.yaml @@ -0,0 +1,23 @@ +publish_channels: + keystone-k8s: latest/edge + glance-k8s: latest/edge + nova-k8s: latest/edge + placement-k8s: latest/edge + neutron-k8s: latest/edge + ovn-central-k8s: latest/edge + ovn-relay-k8s: latest/edge + cinder-k8s: latest/edge + cinder-ceph-k8s: latest/edge + horizon-k8s: latest/edge + heat-k8s: latest/edge + octavia-k8s: latest/edge + aodh-k8s: latest/edge + ceilometer-k8s: latest/edge + gnocchi-k8s: latest/edge + barbican-k8s: latest/edge + designate-k8s: latest/edge + designate-bind-k8s: latest/edge + magnum-k8s: latest/edge + keystone-ldap-k8s: latest/edge + openstack-exporter-k8s: latest/edge + openstack-hypervisor: latest/edge diff --git a/roles/charm-publish/tasks/main.yaml b/roles/charm-publish/tasks/main.yaml new file mode 100644 index 00000000..7a851492 --- /dev/null +++ b/roles/charm-publish/tasks/main.yaml @@ -0,0 +1,53 @@ +- name: Get all job names from gate pipeline + uri: + url: "{{ download_artifact_api }}/builds?{{ download_artifact_query }}" + register: build_output + vars: + download_artifact_api: "https://zuul.opendev.org/api/tenant/{{ zuul.tenant }}" + download_artifact_query: "change={{ zuul.change }}&patchset={{ zuul.patchset }}&pipeline=gate" + +- name: Get relevant charm build jobs + set_fact: + relevant_charm_build_jobs: "{{ build_output.json | selectattr('job_name', 'match', '^charm-build-.*$') | map(attribute='job_name') | list }}" + +- name: Print relevant build jobs + debug: + msg: "Relevant charm build jobs: {{ relevant_charm_build_jobs }}" + +- name: built charm is present locally (artifact from gate pipeline) + include_role: + name: download-artifact + vars: + download_artifact_api: "https://zuul.opendev.org/api/tenant/{{ zuul.tenant }}" + download_artifact_type: charm + download_artifact_pipeline: gate + download_artifact_job: "{{ item }}" + download_artifact_directory: "{{ zuul.project.src_dir }}" + with_items: "{{ relevant_charm_build_jobs }}" + +- name: Get all downloaded charm names + args: + chdir: "{{ zuul.project.src_dir }}" + executable: /bin/bash + shell: | + ls *.charm | cut -d"." -f 1 + register: built_charms + +- name: Prepare charm channel dict for downloaded charms + set_fact: + charm_channels: "{{ charm_channels | default({}) | combine({item.key: item.value}) }}" + loop: "{{ lookup('ansible.builtin.dict', publish_channels) }}" + when: "{{ item.key in built_charms.stdout_lines }}" + +- name: Print charm channel dict + debug: + msg: "Charms to be published: {{ charm_channels }}" + +- name: Publish charms in a loop + include_tasks: "publish.yaml" + vars: + charm_build_name: "{{ channel.key }}" + publish_channel: "{{ channel.value }}" + loop: "{{ charm_channels|dict2items }}" + loop_control: + loop_var: channel diff --git a/roles/charm-publish/tasks/publish.yaml b/roles/charm-publish/tasks/publish.yaml new file mode 100644 index 00000000..7d85d799 --- /dev/null +++ b/roles/charm-publish/tasks/publish.yaml @@ -0,0 +1,59 @@ +- name: Publish charms to charmhub + when: publish_charm + environment: + CHARMCRAFT_AUTH: "{{ charmhub_token.value }}" + block: + - name: Install docker + include_role: + name: ensure-docker + + - name: Upload oci-image to charmhub + register: upload_oci_image_output + vars: + metadata: "{{ lookup('file', zuul.executor.work_root+'/'+zuul.project.src_dir+'/charms/'+charm_build_name+'/metadata.yaml') | from_yaml }}" + args: + executable: /bin/bash + shell: | + set -x + image={{ item.value['upstream-source'] }} + # Remove docker.io/ in the OCI image so that docker pulls image + # from mirror if configured. + image=${image#"docker.io/"} + docker pull $image + digest=`docker inspect --format {% raw %}'{{ index .RepoDigests 0 }}' {% endraw %} $image` + charmcraft upload-resource {{ charm_build_name }} {{ item.key }} --image $digest + retries: 3 + until: > + ("Revision" in upload_oci_image_output.stdout) + loop: "{{ lookup('ansible.builtin.dict', metadata.resources|default({}), wantlist=True) }}" + when: "item.value.type == 'oci-image'" + + - name: Extract Resource revisions + set_fact: + resource_revision_flags: "{{ resource_revision_flags | default('') + ' --resource ' + item.item.key + ':' + (item.stdout | regex_search('Revision ([0-9]+)', '\\1', multiline=True) | first) }}" + with_items: "{{ upload_oci_image_output.results }}" + + - name: Upload charm to charmhub + register: upload_charm_output + args: + chdir: "{{ zuul.project.src_dir }}" + # TODO: The below command can error out with a message that says + # upload with that digest already exists. This case need to be handled. + # More details https://github.com/canonical/charmcraft/issues/826 + command: + charmcraft upload -v --name {{ charm_build_name }} {{ charm_build_name }}.charm + retries: 3 + until: > + ("Revision" in upload_charm_output.stdout) + + - name: Extract Charm revision + set_fact: + charm_revision: "{{ upload_charm_output.stdout | regex_search('Revision ([0-9]+)', '\\1', multiline=True) | first }}" + + - name: Release charm + register: release_charm_output + command: + charmcraft release {{ charm_build_name }} --revision {{ charm_revision }} --channel {{ publish_channel }} {{ resource_revision_flags | default("") }} + retries: 3 + until: > + ("Revision" in release_charm_output.stdout) diff --git a/roles/collect-run-data/tasks/main.yaml b/roles/collect-run-data/tasks/main.yaml new file mode 100644 index 00000000..81123bc5 --- /dev/null +++ b/roles/collect-run-data/tasks/main.yaml @@ -0,0 +1,95 @@ +- name: test runner packages are installed + apt: + name: + - jq + become: true +- name: Create destination for logs + file: + path: "{{ zuul.project.src_dir }}/log" + state: directory + mode: 0755 +- name: collect microk8s inspection report + args: + executable: /bin/bash + shell: | + cp /var/snap/microk8s/current/inspection-report-*.tar.gz "{{ zuul.project.src_dir }}/log/" + failed_when: false +- name: debug logs replay + args: + executable: /bin/bash + shell: | + set -o pipefail + MODEL="$(juju models --format=json | jq -r '.models[]["short-name"]' | grep '^zaza-')" + juju switch $MODEL + juju debug-log --replay > {{ zuul.project.src_dir }}/log/debug-hooks.txt + exit 0 +- name: debug describe pods + args: + executable: /bin/bash + shell: | + set -o pipefail + MODEL="$(juju models --format=json | jq -r '.models[]["short-name"]' | grep '^zaza-')" + microk8s.kubectl describe -n $MODEL pods > {{ zuul.project.src_dir }}/log/describe-pods.txt + CONTROLLER_MODEL="$(microk8s.kubectl get ns | grep controller | awk '{print $1}')" + microk8s.kubectl describe -n $CONTROLLER_MODEL pods > {{ zuul.project.src_dir }}/log/describe-controller-pods.txt + exit 0 +- name: juju status + args: + executable: /bin/bash + shell: | + set -o pipefail + for model in $(juju models | grep zaza- | awk '{gsub(/\*?/,""); print $1}'); do + juju status -m $model > {{ zuul.project.src_dir }}/log/juju-status.$model.txt + juju status -m $model --format=yaml > {{ zuul.project.src_dir }}/log/juju-status.$model.yaml + done +- name: Collect var logs + args: + executable: /bin/bash + shell: | + set -o pipefail + MODEL_NAME=$(juju models --format=json | jq -r '.models[]["short-name"]' | grep '^zaza-') + UNITS=$(juju status --format oneline | awk '{print $2}' | sed -e 's!:!!' | grep -Ev '^$' | paste -s -d' ') + for UNIT_NAME in $UNITS; do + POD_NAME=$(echo $UNIT_NAME | sed -e 's!/!-!') + CONTAINERS=$(microk8s.kubectl get pods -n $MODEL_NAME $POD_NAME -o jsonpath='{.spec.containers[*].name}' | sed -e 's/charm //') + for CONTAINER in $CONTAINERS; do + juju ssh --container $CONTAINER -m $MODEL_NAME $UNIT_NAME "tar zcf /tmp/logs.tgz /var/log/" + juju scp --container $CONTAINER -m $MODEL_NAME $UNIT_NAME:/tmp/logs.tgz {{ zuul.project.src_dir }}/log/$POD_NAME-$CONTAINER.tgz + done + done +- name: Collect pods logs + args: + executable: /bin/bash + shell: | + set -o pipefail + LOG_FOLDER={{ zuul.project.src_dir }}/log/pods/ + MODEL_NAME=$(juju models --format=json | jq -r '.models[]["short-name"]' | grep '^zaza-') + mkdir -p $LOG_FOLDER + for pod in $(microk8s.kubectl get pods -n $MODEL_NAME -o=jsonpath='{.items[*].metadata.name}'); + do + echo Collecting logs: $pod + microk8s.kubectl logs --ignore-errors -n $MODEL_NAME --all-containers $pod > $LOG_FOLDER/$pod.log + done +- name: Collect units' info + args: + executable: /bin/bash + shell: | + set -o pipefail + set -x + LOG_FOLDER={{ zuul.project.src_dir }}/log/unit-info/ + MODEL_NAME=$(juju models --format=json | jq -r '.models[]["short-name"]' | grep '^zaza-') + mkdir -p $LOG_FOLDER + for unit in $(juju status --format json | jq -r '[.applications[].units | keys[0]] | join("\n")'); + do + echo Collecting unit info: $unit + unit_name=$(echo $unit | tr / -) + juju show-unit --output="$LOG_FOLDER/$unit_name.yaml" $unit + done +- name: fetch juju logs + synchronize: + dest: "{{ zuul.executor.log_root }}" + mode: pull + src: "{{ zuul.project.src_dir }}/log" + verify_host: true + owner: false + group: false diff --git a/roles/microk8s-cloud/tasks/main.yaml b/roles/microk8s-cloud/tasks/main.yaml new file mode 100644 index 00000000..60b29b36 --- /dev/null +++ b/roles/microk8s-cloud/tasks/main.yaml @@ -0,0 +1,157 @@ +- name: snapd is installed + apt: + name: snapd + become: true + +- name: set microk8s related variables + set_fact: + microk8s_group: "{{ 'microk8s' if microk8s_classic_mode | default(true) else 'snap_microk8s' }}" + microk8s_command_escalation: "{{ false if microk8s_classic_mode | default(true) else true }}" + +- name: Disable ipv6 + become: true + sysctl: + name: "net.ipv6.conf.all.disable_ipv6" + value: "1" + state: "present" + reload: "yes" + +- name: microk8s is installed + snap: + name: microk8s + classic: "{{ microk8s_classic_mode | default(true) }}" + channel: "{{ microk8s_channel | default('latest/stable') }}" + become: true + +- name: current user is in microk8s group + user: + name: "{{ ansible_user }}" + groups: "{{ microk8s_group }}" + append: true + become: true + +- name: reset ssh connection to apply permissions from new group + meta: reset_connection + +- name: microk8s status + block: + - name: microk8s status + command: + cmd: microk8s status --wait-ready --timeout 300 + rescue: + - name: microk8s inspect + command: + cmd: microk8s inspect + become: "{{ microk8s_command_escalation }}" + - name: microk8s status + command: + # second chance to get status + cmd: microk8s status + +- name: Create docker.io certs dir + when: + - docker_mirror is defined + file: + path: /var/snap/microk8s/current/args/certs.d/docker.io + state: directory + owner: root + group: "{{ microk8s_group }}" + mode: '0770' + +- name: Render microk8s registry mirror template + when: + - docker_mirror is defined + template: + src: hosts.j2 + dest: /var/snap/microk8s/current/args/certs.d/docker.io/hosts.toml + group: "{{ microk8s_group }}" + vars: + mirror_location: "{{ docker_mirror }}" + server: https://docker.io + +- name: Check docker.io hosts.toml + when: + - docker_mirror is defined + command: + cmd: cat /var/snap/microk8s/current/args/certs.d/docker.io/hosts.toml + +- name: microk8s is started + command: + cmd: microk8s start + become: "{{ microk8s_command_escalation }}" + +- name: microk8s is running and ready + command: + cmd: microk8s status --wait-ready + register: res + failed_when: '"is running" not in res.stdout' + +- name: microk8s dns addon is enabled + command: + cmd: microk8s enable dns + register: res + changed_when: '"already enabled" not in res.stdout' + become: "{{ microk8s_command_escalation }}" + +- name: microk8s hostpath storage addon is enabled + command: + cmd: microk8s enable hostpath-storage + register: res + changed_when: '"already enabled" not in res.stdout' + become: "{{ microk8s_command_escalation }}" + +- name: microk8s metallb addon is enabled + command: + # ip range is an arbitrary choice; may need to be changed later + cmd: microk8s enable metallb:10.170.0.1-10.170.0.100 + register: res + changed_when: '"already enabled" not in res.stdout' + become: "{{ microk8s_command_escalation }}" + +- name: microk8s addons are ready + command: + cmd: microk8s status --format short + register: res + retries: 18 + delay: 10 # 18 * 10 = 3 minutes + until: > + "core/dns: enabled" in res.stdout and + "core/hostpath-storage: enabled" in res.stdout and + "core/metallb: enabled" in res.stdout + changed_when: res.attempts > 1 + +- name: juju is installed + snap: + name: juju + classic: "{{ juju_classic_mode | default(true) }}" + channel: "{{ juju_channel | default('latest/stable') }}" + become: true + +- name: Ensure ~/.local/share directory exist + file: + path: ~/.local/share + state: directory + +- name: juju is bootstrapped on microk8s + command: + cmd: juju bootstrap --config bootstrap-timeout=600 microk8s microk8s + register: res + retries: 3 + delay: 10 + until: > + "Bootstrap complete" in res.stderr or + "already exists" in res.stderr + failed_when: '"ERROR" in res.stderr and "already exists" not in res.stderr' + +- name: current juju controller is microk8s + command: + cmd: juju switch microk8s + register: res + changed_when: '"no change" not in res.stderr' + +- name: Collect snap versions + command: snap list + register: snap_out + +- name: Show snap versions + debug: msg="{{ snap_out.stdout }}" diff --git a/roles/microk8s-cloud/templates/hosts.j2 b/roles/microk8s-cloud/templates/hosts.j2 new file mode 100644 index 00000000..a86649af --- /dev/null +++ b/roles/microk8s-cloud/templates/hosts.j2 @@ -0,0 +1,4 @@ +server = "{{ server }}" + +[host."{{ mirror_location }}"] + capabilities = ["pull", "resolve"] diff --git a/roles/zaza-func-test/tasks/main.yaml b/roles/zaza-func-test/tasks/main.yaml new file mode 100644 index 00000000..053d87de --- /dev/null +++ b/roles/zaza-func-test/tasks/main.yaml @@ -0,0 +1,31 @@ +- name: Get all job names + uri: + url: "{{ download_artifact_api }}/builds?{{ download_artifact_query }}" + register: build_output + vars: + download_artifact_api: "https://zuul.opendev.org/api/tenant/{{ zuul.tenant }}" + download_artifact_query: "change={{ zuul.change }}&patchset={{ zuul.patchset }}&pipeline=check" + +- name: Get relevant charm build jobs + set_fact: + relevant_charm_build_jobs: "{{ build_output.json | selectattr('job_name', 'match', '^charm-build-.*$') | map(attribute='job_name') | list | intersect(charm_jobs) }}" + +- name: Print relevant build jobs + debug: + msg: "Relevant charm build jobs: {{ relevant_charm_build_jobs }}" + +- name: built charm is present locally (artifact from previous job) + include_role: + name: download-artifact + vars: + download_artifact_api: "https://zuul.opendev.org/api/tenant/{{ zuul.tenant }}" + download_artifact_type: charm + download_artifact_pipeline: check + download_artifact_job: "{{ item }}" + download_artifact_directory: "{{ zuul.project.src_dir }}" + with_items: "{{ relevant_charm_build_jobs }}" + +- name: run smoke tests + command: + cmd: "{{ tox_executable }} -e func -- --smoke --test-directory={{ test_dir }}" + chdir: "{{ zuul.project.src_dir }}" diff --git a/run_tox.sh b/run_tox.sh new file mode 100755 index 00000000..a82cf957 --- /dev/null +++ b/run_tox.sh @@ -0,0 +1,110 @@ +#!/bin/bash + +source common.sh + +if [[ $1 == "fmt" ]]; +then + src_path_array=$(ls -d -1 "charms/"**/src) + tst_path_array=$(ls -d -1 "charms/"**/tests) + lib_path_array=$(ls -d -1 "charms/"**/lib) + + src_path="${src_path_array[*]}" + tst_path="${tst_path_array[*]}" + lib_path="${lib_path_array[*]}" + + isort ${src_path} ${tst_path} + black --config pyproject.toml ${src_path} ${tst_path} +elif [[ $1 == "pep8" ]]; +then + src_path_array=$(ls -d -1 "charms/"**/src) + tst_path_array=$(ls -d -1 "charms/"**/tests) + + src_path="${src_path_array[*]}" + tst_path="${tst_path_array[*]}" + + codespell ${src_path} ${tst_path} + pflake8 --config pyproject.toml ${src_path} ${tst_path} + isort --check-only --diff ${src_path} ${tst_path} + black --config pyproject.toml --check --diff ${src_path} ${tst_path} +elif [[ $1 =~ ^(py3|py310|py311)$ ]]; +then + # Run py3 on ops-sunbeam + pushd ops-sunbeam + stestr run --slowest || exit 1 + popd + + # Run py3 on all sunbeam charms + charms=($(ls charms)) + for charm in ${charms[@]}; do + push_common_files $charm || exit 1 + pushd charms/$charm + PYTHONPATH=./src:./lib stestr run --slowest || exit 1 + popd + pop_common_files $charm || exit 1 + done +elif [[ $1 == "cover" ]]; +then + coverage erase + + # Run coverage on ops-sunbeam + pushd ops-sunbeam + coverage erase + PYTHON="coverage run --omit .tox/*,tests/*" stestr run --slowest || exit 1 + coverage combine + popd + + # Run coverage on all sunbeam charms + charms=($(ls charms)) + for charm in ${charms[@]}; do + push_common_files $charm || exit 1 + pushd charms/$charm + coverage erase + PYTHONPATH=./src:./lib:../../ops-sunbeam PYTHON="coverage run --omit .tox/*,tests/*,src/templates/*" stestr run --slowest || exit 1 + coverage combine + popd + done + + # Prepare coverage report + coverage combine charms/*/.coverage ops-sunbeam/.coverage + coverage html -d cover + coverage xml -o cover/coverage.xml + coverage report + + # Common files should be deleted after coverage combine + for charm in ${charms[@]}; do + pop_common_files $charm || exit 1 + done +elif [[ $1 == "build" ]]; +then + if [[ $# != 2 ]]; + then + echo "Command format: tox -e build " + exit 1 + fi + + charm=$2 + charms=($(ls charms)) + if [[ ! ${charms[@]} =~ $charm ]]; + then + echo "Argument should be one of ${charms[@]}"; + exit 1 + fi + + push_common_files $charm || exit 1 + + pushd charms/$charm + charmcraft -v pack || exit 1 + if [[ -e "${charm}.charm" ]]; + then + echo "Removing bad downloaded charm maybe?" + rm "${charm}.charm" + fi + echo "Renaming charm ${charm}_*.charm to ${charm}.charm" + mv ${charm}_*.charm ${charm}.charm + popd + + pop_common_files $charm || exit 1 +else + echo "tox argument should be one of pep8, py3, py310, py311, cover"; + exit 1 +fi diff --git a/charms/aodh-k8s/src/templates/parts/database-connection b/templates/parts/database-connection similarity index 55% rename from charms/aodh-k8s/src/templates/parts/database-connection rename to templates/parts/database-connection index 1fd70ce2..21c25eb6 100644 --- a/charms/aodh-k8s/src/templates/parts/database-connection +++ b/templates/parts/database-connection @@ -1,3 +1,5 @@ {% if database.connection -%} connection = {{ database.connection }} +{% else -%} +connection = sqlite:////var/lib/openstack/openstack.db {% endif -%} diff --git a/charms/aodh-k8s/src/templates/parts/identity-data b/templates/parts/identity-data similarity index 100% rename from charms/aodh-k8s/src/templates/parts/identity-data rename to templates/parts/identity-data diff --git a/charms/ceilometer-k8s/src/templates/parts/identity-data-id-creds b/templates/parts/identity-data-id-creds similarity index 100% rename from charms/ceilometer-k8s/src/templates/parts/identity-data-id-creds rename to templates/parts/identity-data-id-creds diff --git a/charms/magnum-k8s/src/templates/parts/section-certificates b/templates/parts/section-certificates similarity index 100% rename from charms/magnum-k8s/src/templates/parts/section-certificates rename to templates/parts/section-certificates diff --git a/charms/aodh-k8s/src/templates/parts/section-database b/templates/parts/section-database similarity index 100% rename from charms/aodh-k8s/src/templates/parts/section-database rename to templates/parts/section-database diff --git a/charms/aodh-k8s/src/templates/parts/section-federation b/templates/parts/section-federation similarity index 100% rename from charms/aodh-k8s/src/templates/parts/section-federation rename to templates/parts/section-federation diff --git a/charms/aodh-k8s/src/templates/parts/section-identity b/templates/parts/section-identity similarity index 100% rename from charms/aodh-k8s/src/templates/parts/section-identity rename to templates/parts/section-identity diff --git a/charms/aodh-k8s/src/templates/parts/section-middleware b/templates/parts/section-middleware similarity index 100% rename from charms/aodh-k8s/src/templates/parts/section-middleware rename to templates/parts/section-middleware diff --git a/charms/keystone-k8s/src/templates/parts/section-oslo-cache b/templates/parts/section-oslo-cache similarity index 100% rename from charms/keystone-k8s/src/templates/parts/section-oslo-cache rename to templates/parts/section-oslo-cache diff --git a/charms/aodh-k8s/src/templates/parts/section-oslo-messaging-rabbit b/templates/parts/section-oslo-messaging-rabbit similarity index 100% rename from charms/aodh-k8s/src/templates/parts/section-oslo-messaging-rabbit rename to templates/parts/section-oslo-messaging-rabbit diff --git a/charms/keystone-k8s/src/templates/parts/section-oslo-middleware b/templates/parts/section-oslo-middleware similarity index 100% rename from charms/keystone-k8s/src/templates/parts/section-oslo-middleware rename to templates/parts/section-oslo-middleware diff --git a/charms/cinder-ceph-k8s/src/templates/parts/section-oslo-notifications b/templates/parts/section-oslo-notifications similarity index 100% rename from charms/cinder-ceph-k8s/src/templates/parts/section-oslo-notifications rename to templates/parts/section-oslo-notifications diff --git a/charms/aodh-k8s/src/templates/parts/section-service-credentials b/templates/parts/section-service-credentials similarity index 100% rename from charms/aodh-k8s/src/templates/parts/section-service-credentials rename to templates/parts/section-service-credentials diff --git a/charms/ceilometer-k8s/src/templates/parts/section-service-credentials b/templates/parts/section-service-credentials-from-identity-service similarity index 100% rename from charms/ceilometer-k8s/src/templates/parts/section-service-credentials rename to templates/parts/section-service-credentials-from-identity-service diff --git a/charms/barbican-k8s/src/templates/parts/section-service-user b/templates/parts/section-service-user similarity index 100% rename from charms/barbican-k8s/src/templates/parts/section-service-user rename to templates/parts/section-service-user diff --git a/charms/ceilometer-k8s/src/templates/parts/section-service-user-id-creds b/templates/parts/section-service-user-from-identity-credentials similarity index 100% rename from charms/ceilometer-k8s/src/templates/parts/section-service-user-id-creds rename to templates/parts/section-service-user-from-identity-credentials diff --git a/charms/aodh-k8s/src/templates/parts/section-signing b/templates/parts/section-signing similarity index 100% rename from charms/aodh-k8s/src/templates/parts/section-signing rename to templates/parts/section-signing diff --git a/charms/magnum-k8s/src/templates/parts/section-trust b/templates/parts/section-trust similarity index 100% rename from charms/magnum-k8s/src/templates/parts/section-trust rename to templates/parts/section-trust diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 00000000..4cdc9db0 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,17 @@ +mock +stestr +coverage + +tenacity # ops-sunbeam +ops # all charms +lightkube # almost all charms +pwgen # keystone-k8s +python-keystoneclient # keystone-k8s +cryptography # neutron-k8s +jsonschema # neutron-k8s +pytest-interface-tester # barbican-k8s +requests # cinder-ceph-k8s +netifaces # cinder-ceph-k8s +cosl # openstack-exporter +git+https://github.com/juju/charm-helpers.git#egg=charmhelpers # cinder-ceph-k8s,glance-k8s,gnocchi-k8s +git+https://opendev.org/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client # cinder-ceph-k8s diff --git a/tests/caas/smoke.yaml.j2 b/tests/caas/smoke.yaml.j2 new file mode 100644 index 00000000..9a26899b --- /dev/null +++ b/tests/caas/smoke.yaml.j2 @@ -0,0 +1,190 @@ +bundle: kubernetes + +applications: + traefik: + charm: ch:traefik-k8s + channel: 1.0/candidate + scale: 1 + trust: true + options: + kubernetes-service-annotations: metallb.universe.tf/address-pool=public + mysql: + charm: ch:mysql-k8s + channel: 8.0/stable + scale: 1 + trust: false + vault: + charm: ch:vault-k8s + channel: latest/edge + scale: 1 + trust: false + tls-operator: + charm: self-signed-certificates + channel: latest/beta + scale: 1 + options: + ca-common-name: internal-ca + rabbitmq: + charm: ch:rabbitmq-k8s + channel: 3.12/edge + scale: 1 + trust: true + options: + minimum-replicas: 1 + ovn-central: + {% if ovn_central_k8s is defined and ovn_central_k8s is sameas true -%} + charm: ../../../ovn-central-k8s.charm + {% else -%} + charm: ch:ovn-central-k8s + channel: 23.03/edge + {% endif -%} + scale: 1 + trust: true + resources: + ovn-sb-db-server-image: ghcr.io/canonical/ovn-consolidated:23.09 + ovn-nb-db-server-image: ghcr.io/canonical/ovn-consolidated:23.09 + ovn-northd-image: ghcr.io/canonical/ovn-consolidated:23.09 + keystone: + {% if keystone_k8s is defined and keystone_k8s is sameas true -%} + charm: ../../../keystone-k8s.charm + {% else -%} + charm: ch:keystone-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + options: + admin-role: admin + storage: + fernet-keys: 5M + credential-keys: 5M + resources: + keystone-image: ghcr.io/canonical/keystone:2023.2 + glance: + {% if glance_k8s is defined and glance_k8s is sameas true -%} + charm: ../../../glance-k8s.charm + {% else -%} + charm: ch:glance-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + storage: + local-repository: 5G + resources: + glance-api-image: ghcr.io/canonical/glance-api:2023.2 + heat: + {% if heat_k8s is defined and heat_k8s is sameas true -%} + charm: ../../../heat-k8s.charm + {% else -%} + charm: ch:heat-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + resources: + heat-api-image: ghcr.io/canonical/heat-consolidated:2023.2 + heat-engine-image: ghcr.io/canonical/heat-consolidated:2023.2 + octavia: + {% if octavia_k8s is defined and octavia_k8s is sameas true -%} + charm: ../../../octavia-k8s.charm + {% else -%} + charm: ch:octavia-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + resources: + octavia-api-image: ghcr.io/canonical/octavia-consolidated:2023.2 + octavia-driver-agent-image: ghcr.io/canonical/octavia-consolidated:2023.2 + octavia-housekeeping-image: ghcr.io/canonical/octavia-consolidated:2023.2 + barbican: + {% if barbican_k8s is defined and barbican_k8s is sameas true -%} + charm: ../../../barbican-k8s.charm + {% else -%} + charm: ch:barbican-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: false + resources: + barbican-api-image: ghcr.io/canonical/barbican-consolidated:2023.2 + barbican-worker-image: ghcr.io/canonical/barbican-consolidated:2023.2 + magnum: + {% if magnum_k8s is defined and magnum_k8s is sameas true -%} + charm: ../../../magnum-k8s.charm + {% else -%} + charm: ch:magnum-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: false + resources: + magnum-api-image: ghcr.io/canonical/magnum-consolidated:2023.2 + magnum-conductor-image: ghcr.io/canonical/magnum-consolidated:2023.2 + +relations: +- - tls-operator:certificates + - ovn-central:certificates + +- - mysql:database + - keystone:database +- - traefik:ingress + - keystone:ingress-public + +- - mysql:database + - glance:database +- - keystone:identity-service + - glance:identity-service +- - rabbitmq:amqp + - glance:amqp +- - traefik:ingress + - glance:ingress-public + +- - mysql:database + - heat:database +- - keystone:identity-service + - heat:identity-service +- - keystone:identity-ops + - heat:identity-ops +- - traefik:traefik-route + - heat:traefik-route-public +- - rabbitmq:amqp + - heat:amqp + +- - mysql:database + - octavia:database +- - keystone:identity-service + - octavia:identity-service +- - keystone:identity-ops + - octavia:identity-ops +- - traefik:ingress + - octavia:ingress-public +- - tls-operator:certificates + - octavia:certificates +- - octavia:ovsdb-cms + - ovn-central:ovsdb-cms + +- - mysql:database + - barbican:database +- - rabbitmq:amqp + - barbican:amqp +- - keystone:identity-service + - barbican:identity-service +- - keystone:identity-ops + - barbican:identity-ops +- - traefik:ingress + - barbican:ingress-public +- - vault:vault-kv + - barbican:vault-kv + +- - mysql:database + - magnum:database +- - rabbitmq:amqp + - magnum:amqp +- - keystone:identity-service + - magnum:identity-service +- - keystone:identity-ops + - magnum:identity-ops +- - traefik:ingress + - magnum:ingress-public diff --git a/tests/caas/tests.yaml b/tests/caas/tests.yaml new file mode 100644 index 00000000..4c2cd46b --- /dev/null +++ b/tests/caas/tests.yaml @@ -0,0 +1,56 @@ +gate_bundles: + - smoke +smoke_bundles: + - smoke +configure: + - zaza.openstack.charm_tests.keystone.setup.wait_for_all_endpoints + - zaza.openstack.charm_tests.keystone.setup.add_tempest_roles +tests: + - zaza.openstack.charm_tests.tempest.tests.TempestTestWithKeystoneMinimal +tests_options: + trust: + - smoke + ignore_hard_deploy_errors: + - smoke + + tempest: + default: + smoke: True + +target_deploy_status: + traefik: + workload-status: active + workload-status-message-regex: '^$' + mysql: + workload-status: active + workload-status-message-regex: '^.*$' + vault: + workload-status: active + workload-status-message-regex: '^$' + tls-operator: + workload-status: active + workload-status-message-regex: '^$' + rabbitmq: + workload-status: active + workload-status-message-regex: '^$' + ovn-central: + workload-status: active + workload-status-message-regex: '^$' + keystone: + workload-status: active + workload-status-message-regex: '^$' + glance: + workload-status: active + workload-status-message-regex: '^$' + heat: + workload-status: active + workload-status-message-regex: '^.*$' + octavia: + workload-status: active + workload-status-message-regex: '^$' + barbican: + workload-status: active + workload-status-message-regex: '^$' + magnum: + workload-status: active + workload-status-message-regex: '^$' diff --git a/tests/ceph/smoke.yaml.j2 b/tests/ceph/smoke.yaml.j2 new file mode 100644 index 00000000..82fff4cc --- /dev/null +++ b/tests/ceph/smoke.yaml.j2 @@ -0,0 +1,145 @@ +bundle: kubernetes + +applications: + traefik: + charm: ch:traefik-k8s + channel: 1.0/candidate + scale: 1 + trust: true + options: + kubernetes-service-annotations: metallb.universe.tf/address-pool=public + mysql: + charm: ch:mysql-k8s + channel: 8.0/stable + scale: 1 + trust: false + rabbitmq: + charm: ch:rabbitmq-k8s + channel: 3.12/edge + scale: 1 + trust: true + options: + minimum-replicas: 1 + keystone: + {% if keystone_k8s is defined and keystone_k8s is sameas true -%} + charm: ../../../keystone-k8s.charm + {% else -%} + charm: ch:keystone-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + options: + admin-role: admin + storage: + fernet-keys: 5M + credential-keys: 5M + resources: + keystone-image: ghcr.io/canonical/keystone:2023.2 + cinder: + {% if cinder_k8s is defined and cinder_k8s is sameas true -%} + charm: ../../../cinder-k8s.charm + {% else -%} + charm: ch:cinder-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + resources: + cinder-api-image: ghcr.io/canonical/cinder-consolidated:2023.2 + cinder-scheduler-image: ghcr.io/canonical/cinder-consolidated:2023.2 + cinder-ceph: + {% if cinder_ceph_k8s is defined and cinder_ceph_k8s is sameas true -%} + charm: ../../../cinder-ceph-k8s.charm + {% else -%} + charm: ch:cinder-ceph-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + resources: + cinder-volume-image: ghcr.io/canonical/cinder-consolidated:2023.2 + gnocchi: + {% if gnocchi_k8s is defined and gnocchi_k8s is sameas true -%} + charm: ../../../gnocchi-k8s.charm + {% else -%} + charm: ch:gnocchi-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + resources: + gnocchi-api-image: ghcr.io/canonical/gnocchi-consolidated:2023.1 + gnocchi-metricd-image: ghcr.io/canonical/gnocchi-consolidated:2023.1 + ceilometer: + {% if ceilometer_k8s is defined and ceilometer_k8s is sameas true -%} + charm: ../../../ceilometer-k8s.charm + {% else -%} + charm: ch:ceilometer-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + resources: + ceilometer-central-image: ghcr.io/canonical/ceilometer-consolidated:2023.2 + ceilometer-notification-image: ghcr.io/canonical/ceilometer-consolidated:2023.2 + aodh: + {% if aodh_k8s is defined and aodh_k8s is sameas true -%} + charm: ../../../aodh-k8s.charm + {% else -%} + charm: ch:aodh-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + resources: + aodh-api-image: ghcr.io/canonical/aodh-consolidated:2023.2 + aodh-evaluator-image: ghcr.io/canonical/aodh-consolidated:2023.2 + aodh-notifier-image: ghcr.io/canonical/aodh-consolidated:2023.2 + aodh-listener-image: ghcr.io/canonical/aodh-consolidated:2023.2 + aodh-expirer-image: ghcr.io/canonical/aodh-consolidated:2023.2 + +relations: +- - mysql:database + - keystone:database +- - traefik:ingress + - keystone:ingress-public + +- - mysql:database + - cinder:database +- - cinder:amqp + - rabbitmq:amqp +- - keystone:identity-service + - cinder:identity-service +- - traefik:ingress + - cinder:ingress-public + +- - cinder-ceph:database + - mysql:database +- - cinder-ceph:amqp + - rabbitmq:amqp +- - cinder:storage-backend + - cinder-ceph:storage-backend + +- - mysql:database + - gnocchi:database +- - traefik:ingress + - gnocchi:ingress-public +- - keystone:identity-service + - gnocchi:identity-service + +- - rabbitmq:amqp + - ceilometer:amqp +- - keystone:identity-credentials + - ceilometer:identity-credentials +- - gnocchi:gnocchi-service + - ceilometer:gnocchi-db + +- - mysql:database + - aodh:database +- - rabbitmq:amqp + - aodh:amqp +- - keystone:identity-service + - aodh:identity-service +- - traefik:ingress + - aodh:ingress-public diff --git a/tests/ceph/tests.yaml b/tests/ceph/tests.yaml new file mode 100644 index 00000000..caec8bdc --- /dev/null +++ b/tests/ceph/tests.yaml @@ -0,0 +1,43 @@ +gate_bundles: + - smoke +smoke_bundles: + - smoke +# There is no storage provider at the moment so cannot run tests. +configure: + - zaza.charm_tests.noop.setup.basic_setup +tests: + - zaza.charm_tests.noop.tests.NoopTest +tests_options: + trust: + - smoke + ignore_hard_deploy_errors: + - smoke + +target_deploy_status: + traefik: + workload-status: active + workload-status-message-regex: '^$' + mysql: + workload-status: active + workload-status-message-regex: '^.*$' + rabbitmq: + workload-status: active + workload-status-message-regex: '^$' + keystone: + workload-status: active + workload-status-message-regex: '^$' + cinder: + workload-status: active + workload-status-message-regex: '^$' + cinder-ceph: + workload-status: blocked + workload-status-message-regex: '^.*ceph.*$' + ceilometer: + workload-status: waiting + workload-status-message-regex: '^.*Not all relations are ready$' + aodh: + workload-status: active + workload-status-message-regex: '^.*$' + gnocchi: + workload-status: blocked + workload-status-message-regex: '^.*ceph.*$' diff --git a/tests/core/smoke.yaml.j2 b/tests/core/smoke.yaml.j2 new file mode 100644 index 00000000..4598ddf3 --- /dev/null +++ b/tests/core/smoke.yaml.j2 @@ -0,0 +1,192 @@ +bundle: kubernetes + +applications: + traefik: + charm: ch:traefik-k8s + channel: 1.0/candidate + scale: 1 + trust: true + options: + kubernetes-service-annotations: metallb.universe.tf/address-pool=public + mysql: + charm: ch:mysql-k8s + channel: 8.0/stable + scale: 1 + trust: false + tls-operator: + charm: self-signed-certificates + channel: latest/beta + scale: 1 + options: + ca-common-name: internal-ca + rabbitmq: + charm: ch:rabbitmq-k8s + channel: 3.12/edge + scale: 1 + trust: true + options: + minimum-replicas: 1 + ovn-central: + {% if ovn_central_k8s is defined and ovn_central_k8s is sameas true -%} + charm: ../../../ovn-central-k8s.charm + {% else -%} + charm: ch:ovn-central-k8s + channel: 23.03/edge + {% endif -%} + scale: 1 + trust: true + resources: + ovn-sb-db-server-image: ghcr.io/canonical/ovn-consolidated:23.09 + ovn-nb-db-server-image: ghcr.io/canonical/ovn-consolidated:23.09 + ovn-northd-image: ghcr.io/canonical/ovn-consolidated:23.09 + ovn-relay: + {% if ovn_relay_k8s is defined and ovn_relay_k8s is sameas true -%} + charm: ../../../ovn-relay-k8s.charm + {% else -%} + charm: ch:ovn-relay-k8s + channel: 23.03/edge + {% endif -%} + scale: 1 + trust: true + resources: + ovn-sb-db-server-image: ghcr.io/canonical/ovn-consolidated:23.09 + keystone: + {% if keystone_k8s is defined and keystone_k8s is sameas true -%} + charm: ../../../keystone-k8s.charm + {% else -%} + charm: ch:keystone-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + options: + admin-role: admin + storage: + fernet-keys: 5M + credential-keys: 5M + resources: + keystone-image: ghcr.io/canonical/keystone:2023.2 + glance: + {% if glance_k8s is defined and glance_k8s is sameas true -%} + charm: ../../../glance-k8s.charm + {% else -%} + charm: ch:glance-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + storage: + local-repository: 5G + resources: + glance-api-image: ghcr.io/canonical/glance-api:2023.2 + nova: + {% if nova_k8s is defined and nova_k8s is sameas true -%} + charm: ../../../nova-k8s.charm + {% else -%} + charm: ch:nova-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + resources: + nova-api-image: ghcr.io/canonical/nova-consolidated:2023.2 + nova-scheduler-image: ghcr.io/canonical/nova-consolidated:2023.2 + nova-conductor-image: ghcr.io/canonical/nova-consolidated:2023.2 + placement: + {% if placement_k8s is defined and placement_k8s is sameas true -%} + charm: ../../../placement-k8s.charm + {% else -%} + charm: ch:placement-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + resources: + placement-api-image: ghcr.io/canonical/placement-api:2023.2 + neutron: + {% if neutron_k8s is defined and neutron_k8s is sameas true -%} + charm: ../../../neutron-k8s.charm + {% else -%} + charm: ch:neutron-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + options: + debug: true + resources: + neutron-server-image: ghcr.io/canonical/neutron-server:2023.2 + horizon: + {% if horizon_k8s is defined and horizon_k8s is sameas true -%} + charm: ../../../horizon-k8s.charm + {% else -%} + charm: ch:horizon-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + resources: + horizon-image: ghcr.io/canonical/horizon:2023.2 + +relations: +- - tls-operator:certificates + - ovn-central:certificates + +- - tls-operator:certificates + - ovn-relay:certificates +- - ovn-relay:ovsdb-cms + - ovn-central:ovsdb-cms + +- - mysql:database + - keystone:database +- - traefik:ingress + - keystone:ingress-public + +- - mysql:database + - glance:database +- - keystone:identity-service + - glance:identity-service +- - rabbitmq:amqp + - glance:amqp +- - traefik:ingress + - glance:ingress-public + +- - mysql:database + - nova:database +- - mysql:database + - nova:api-database +- - mysql:database + - nova:cell-database +- - rabbitmq:amqp + - nova:amqp +- - keystone:identity-service + - nova:identity-service +- - traefik:ingress + - nova:ingress-public + +- - mysql:database + - placement:database +- - keystone:identity-service + - placement:identity-service +- - traefik:ingress + - placement:ingress-public + +- - mysql:database + - neutron:database +- - rabbitmq:amqp + - neutron:amqp +- - keystone:identity-service + - neutron:identity-service +- - traefik:ingress + - neutron:ingress-public +- - tls-operator:certificates + - neutron:certificates +- - neutron:ovsdb-cms + - ovn-central:ovsdb-cms + +- - mysql:database + - horizon:database +- - keystone:identity-credentials + - horizon:identity-credentials +- - traefik:ingress + - horizon:ingress-public diff --git a/tests/core/tests.yaml b/tests/core/tests.yaml new file mode 100644 index 00000000..821a7d54 --- /dev/null +++ b/tests/core/tests.yaml @@ -0,0 +1,72 @@ +gate_bundles: + - smoke +smoke_bundles: + - smoke +configure: + - zaza.openstack.charm_tests.keystone.setup.wait_for_all_endpoints + - zaza.openstack.charm_tests.keystone.setup.add_tempest_roles + - zaza.openstack.charm_tests.nova.setup.create_flavors + - zaza.openstack.charm_tests.nova.setup.manage_ssh_key +tests: + - zaza.openstack.charm_tests.tempest.tests.TempestTestWithKeystoneMinimal +tests_options: + trust: + - smoke + ignore_hard_deploy_errors: + - smoke + + tempest: + default: + smoke: True + exclude-list: + - "tempest.api.image.v2.test_images.BasicOperationsImagesTest.test_register_upload_get_image_file" + - "tempest.api.compute.security_groups.test_security_group_rules.SecurityGroupRulesTestJSON.test_security_group_rules_create" + - "tempest.api.compute.security_groups.test_security_group_rules.SecurityGroupRulesTestJSON.test_security_group_rules_list" + - "tempest.api.compute.security_groups.test_security_groups.SecurityGroupsTestJSON.test_security_groups_create_list_delete" + - "tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON" + - "tempest.api.compute.servers.test_create_server.ServersTestManualDisk" + - "tempest.api.compute.servers.test_server_addresses.ServerAddressesTestJSON" + - "tempest.api.compute.servers.test_create_server.ServersTestJSON" + - "tempest.scenario.test_server_multinode.TestServerMultinode.test_schedule_to_all_nodes" + - "tempest.scenario.test_server_basic_ops.TestServerBasicOps.test_server_basic_ops" + - "tempest.api.compute.servers.test_attach_interfaces.AttachInterfacesUnderV243Test.test_add_remove_fixed_ip" + include-list: + - "tempest.api.identity.v3.test_application_credentials.ApplicationCredentialsV3Test.test_create_application_credential" + +target_deploy_status: + traefik: + workload-status: active + workload-status-message-regex: '^$' + mysql: + workload-status: active + workload-status-message-regex: '^.*$' + tls-operator: + workload-status: active + workload-status-message-regex: '^$' + rabbitmq: + workload-status: active + workload-status-message-regex: '^$' + ovn-central: + workload-status: active + workload-status-message-regex: '^$' + ovn-relay: + workload-status: active + workload-status-message-regex: '^$' + keystone: + workload-status: active + workload-status-message-regex: '^$' + glance: + workload-status: active + workload-status-message-regex: '^$' + nova: + workload-status: active + workload-status-message-regex: '^$' + placement: + workload-status: active + workload-status-message-regex: '^$' + neutron: + workload-status: active + workload-status-message-regex: '^$' + horizon: + workload-status: active + workload-status-message-regex: '^$' diff --git a/tests/misc/smoke.yaml.j2 b/tests/misc/smoke.yaml.j2 new file mode 100644 index 00000000..6ae7253a --- /dev/null +++ b/tests/misc/smoke.yaml.j2 @@ -0,0 +1,105 @@ +bundle: kubernetes + +applications: + traefik: + charm: ch:traefik-k8s + channel: 1.0/candidate + scale: 1 + trust: true + options: + kubernetes-service-annotations: metallb.universe.tf/address-pool=public + mysql: + charm: ch:mysql-k8s + channel: 8.0/stable + scale: 1 + trust: false + ldap-server: + charm: ch:ldap-test-fixture-k8s + channel: edge + scale: 1 + rabbitmq: + charm: ch:rabbitmq-k8s + channel: 3.12/edge + scale: 1 + trust: true + options: + minimum-replicas: 1 + keystone: + {% if keystone_k8s is defined and keystone_k8s is sameas true -%} + charm: ../../../keystone-k8s.charm + {% else -%} + charm: ch:keystone-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: true + options: + admin-role: admin + storage: + fernet-keys: 5M + credential-keys: 5M + resources: + keystone-image: ghcr.io/canonical/keystone:2023.2 + designate-bind: + {% if designate_bind_k8s is defined and designate_bind_k8s is sameas true -%} + charm: ../../../designate-bind-k8s.charm + {% else -%} + charm: ch:designate-bind-k8s + channel: 9/edge + {% endif -%} + scale: 1 + trust: false + resources: + designate-bind-image: ubuntu/bind9:9.18-22.04_beta + designate: + {% if designate_k8s is defined and designate_k8s is sameas true -%} + charm: ../../../designate-k8s.charm + {% else -%} + charm: ch:designate-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + trust: false + resources: + designate-image: ghcr.io/canonical/designate-consolidated:2023.2 + keystone-ldap: + {% if keystone_ldap_k8s is defined and keystone_ldap_k8s is sameas true -%} + charm: ../../../keystone-ldap-k8s.charm + {% else -%} + charm: ch:keystone-ldap-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + openstack-exporter: + {% if openstack_exporter_k8s is defined and openstack_exporter_k8s is sameas true -%} + charm: ../../../openstack-exporter-k8s.charm + {% else -%} + charm: ch:openstack-exporter-k8s + channel: 2023.2/edge + {% endif -%} + scale: 1 + resources: + openstack-exporter-image: ghcr.io/canonical/openstack-exporter:1.6.0-7533071 + +relations: +- - mysql:database + - keystone:database +- - traefik:ingress + - keystone:ingress-public + +- - mysql:database + - designate:database +- - rabbitmq:amqp + - designate:amqp +- - keystone:identity-service + - designate:identity-service +- - traefik:ingress + - designate:ingress-public +- - designate-bind:dns-backend + - designate:dns-backend + +- - keystone:domain-config + - keystone-ldap:domain-config + +- - keystone:identity-ops + - openstack-exporter:identity-ops diff --git a/tests/misc/tests.yaml b/tests/misc/tests.yaml new file mode 100644 index 00000000..647232b4 --- /dev/null +++ b/tests/misc/tests.yaml @@ -0,0 +1,52 @@ +gate_bundles: + - smoke +smoke_bundles: + - smoke +configure: + - zaza.charm_tests.noop.setup.basic_setup + # https://bugs.launchpad.net/snap-openstack/+bug/2045206 + # - zaza.openstack.charm_tests.keystone.setup.wait_for_all_endpoints +tests: + - zaza.charm_tests.noop.tests.NoopTest + # Tests commented until bug fix for https://bugs.launchpad.net/snap-openstack/+bug/2045206 + # - zaza.openstack.charm_tests.tempest.tests.TempestTestWithKeystoneMinimal + # - zaza.openstack.charm_tests.keystone.tests_ldap_k8s.LdapExplicitCharmConfigTestsK8S + # - zaza.openstack.charm_tests.openstack_exporter.tests.OpenstackExporterTest +tests_options: + trust: + - smoke + ignore_hard_deploy_errors: + - smoke + + tempest: + default: + smoke: True + +target_deploy_status: + traefik: + workload-status: active + workload-status-message-regex: '^$' + mysql: + workload-status: active + workload-status-message-regex: '^.*$' + ldap-server: + workload-status: active + workload-status-message-regex: '^$' + rabbitmq: + workload-status: active + workload-status-message-regex: '^$' + keystone: + workload-status: waiting + workload-status-message-regex: '^.*domain-config.*integration incomplete.*$|^$' + designate-bind: + workload-status: active + workload-status-message-regex: '^.*$' + designate: + workload-status: active + workload-status-message-regex: '^.*$' + keystone-ldap: + workload-status: active + workload-status-message-regex: '^$' + openstack-exporter: + workload-status: active + workload-status-message-regex: '^$' diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..2d2868ac --- /dev/null +++ b/tox.ini @@ -0,0 +1,93 @@ +# Global tox file + +# This file is used to invoke tox in individual charms + +[tox] +skipsdist = True +envlist = pep8,py3 +sitepackages = False +skip_missing_interpreters = False +minversion = 3.18.0 + +[testenv] +passenv = + HOME +allowlist_externals = + {toxinidir}/run_tox.sh + {toxinidir}/fetch_libs.sh + +[testenv:fetch] +basepython = python3 +deps = +commands = + {toxinidir}/fetch_libs.sh + +[testenv:fmt] +description = Apply coding style standards to code +deps = + black + isort +commands = + {toxinidir}/run_tox.sh fmt + +[testenv:pep8] +description = Alias for lint +deps = + black + flake8<6 + flake8-docstrings + flake8-copyright + flake8-builtins + pyproject-flake8 + pep8-naming + isort + codespell +commands = + {toxinidir}/run_tox.sh pep8 + +[testenv:py3] +deps = + -r{toxinidir}/test-requirements.txt +commands = + {toxinidir}/run_tox.sh py3 + +[testenv:py310] +deps = {[testenv:py3]deps} +commands = + {toxinidir}/run_tox.sh py310 + +[testenv:py311] +deps = {[testenv:py3]deps} +commands = + {toxinidir}/run_tox.sh py311 + +[testenv:cover] +deps = {[testenv:py3]deps} +commands = + {toxinidir}/run_tox.sh cover + +[testenv:build] +basepython = python3 +deps = +commands = + {toxinidir}/run_tox.sh build {posargs} + +[testenv:func-noop] +basepython = python3 +deps = + git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza + git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack + git+https://opendev.org/openstack/tempest.git#egg=tempest +commands = + functest-run-suite --help + +[testenv:func] +basepython = python3 +deps = {[testenv:func-noop]deps} +setenv = + TEST_MODEL_SETTINGS = automatically-retry-hooks=true + TEST_MAX_RESOLVE_COUNT = 5 +commands = + python3 render_bundles.py + # Example: functest-run-suite --keep-model --smoke --test-directory=tests/set1 + functest-run-suite --keep-model {posargs} diff --git a/zuul.d/jobs.yaml b/zuul.d/jobs.yaml new file mode 100644 index 00000000..648e8a3c --- /dev/null +++ b/zuul.d/jobs.yaml @@ -0,0 +1,410 @@ +- job: + name: charm-build-keystone-k8s + description: Build sunbeam keystone-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/keystone-k8s/* + vars: + charm: keystone-k8s +- job: + name: charm-build-glance-k8s + description: Build sunbeam glance-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/glance-k8s/* + vars: + charm: glance-k8s +- job: + name: charm-build-nova-k8s + description: Build sunbeam nova-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/nova-k8s/* + vars: + charm: nova-k8s +- job: + name: charm-build-placement-k8s + description: Build sunbeam placement-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/placement-k8s/* + vars: + charm: placement-k8s +- job: + name: charm-build-neutron-k8s + description: Build sunbeam neutron-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/neutron-k8s/* + vars: + charm: neutron-k8s +- job: + name: charm-build-ovn-central-k8s + description: Build sunbeam ovn-central-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/ovn-central-k8s/* + vars: + charm: ovn-central-k8s +- job: + name: charm-build-ovn-relay-k8s + description: Build sunbeam ovn-relay-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/ovn-relay-k8s/* + vars: + charm: ovn-relay-k8s +- job: + name: charm-build-cinder-k8s + description: Build sunbeam cinder-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/cinder-k8s/* + vars: + charm: cinder-k8s +- job: + name: charm-build-cinder-ceph-k8s + description: Build sunbeam cinder-ceph-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/cinder-ceph-k8s/* + vars: + charm: cinder-ceph-k8s +- job: + name: charm-build-horizon-k8s + description: Build sunbeam horizon-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/horizon-k8s/* + vars: + charm: horizon-k8s +- job: + name: charm-build-heat-k8s + description: Build sunbeam heat-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/heat-k8s/* + vars: + charm: heat-k8s +- job: + name: charm-build-octavia-k8s + description: Build sunbeam octavia-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/octavia-k8s/* + vars: + charm: octavia-k8s +- job: + name: charm-build-aodh-k8s + description: Build sunbeam aodh-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/aodh-k8s/* + vars: + charm: aodh-k8s +- job: + name: charm-build-ceilometer-k8s + description: Build sunbeam ceilometer-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/ceilometer-k8s/* + vars: + charm: ceilometer-k8s +- job: + name: charm-build-gnocchi-k8s + description: Build sunbeam gnocchi-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/gnocchi-k8s/* + vars: + charm: gnocchi-k8s +- job: + name: charm-build-barbican-k8s + description: Build sunbeam barbican-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/barbican-k8s/* + vars: + charm: barbican-k8s +- job: + name: charm-build-magnum-k8s + description: Build sunbeam magnum-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/magnum-k8s/* + vars: + charm: magnum-k8s +- job: + name: charm-build-designate-k8s + description: Build sunbeam designate-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/designate-k8s/* + vars: + charm: designate-k8s +- job: + name: charm-build-designate-bind-k8s + description: Build sunbeam designate-bind-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/designate-bind-k8s/* + vars: + charm: designate-bind-k8s +- job: + name: charm-build-keystone-ldap-k8s + description: Build sunbeam keystone-ldap-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/keystone-ldap-k8s/* + vars: + charm: keystone-ldap-k8s +- job: + name: charm-build-openstack-exporter-k8s + description: Build sunbeam openstack-exporter-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/openstack-exporter-k8s/* + vars: + charm: openstack-exporter-k8s +- job: + name: charm-build-openstack-hypervisor + description: Build sunbeam openstack-hypervisor charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/* + - charms/openstack-hypervisor/* + vars: + charm: openstack-hypervisor + +- job: + name: func-test-core + description: | + Zaza smoke test for all the core sunbeam charms. + timeout: 3600 + run: playbooks/zaza-func-test.yaml + post-run: playbooks/collect-run-data.yaml + dependencies: + - name: charm-build-keystone-k8s + soft: true + - name: charm-build-glance-k8s + soft: true + - name: charm-build-nova-k8s + soft: true + - name: charm-build-placement-k8s + soft: true + - name: charm-build-neutron-k8s + soft: true + - name: charm-build-ovn-central-k8s + soft: true + - name: charm-build-ovn-relay-k8s + soft: true + - name: charm-build-horizon-k8s + soft: true + files: + - ops-sunbeam/ops_sunbeam/* + - charms/keystone-k8s/* + - charms/glance-k8s/* + - charms/nova-k8s/* + - charms/neutron-k8s/* + - charms/placement-k8s/* + - charms/ovn-central-k8s/* + - charms/ovn-relay-k8s/* + - charms/horizon-k8s/* + vars: + # Artifacts will be downloaded from below charm jobs + charm_jobs: + - charm-build-keystone-k8s + - charm-build-glance-k8s + - charm-build-nova-k8s + - charm-build-placement-k8s + - charm-build-neutron-k8s + - charm-build-ovn-central-k8s + - charm-build-ovn-relay-k8s + - charm-build-horizon-k8s + # test_dir relative to project src dir + test_dir: tests/core +- job: + name: func-test-ceph + description: | + Zaza smoke test for all the sunbeam charms that + requires storage/ceph. + timeout: 3600 + run: playbooks/zaza-func-test.yaml + post-run: playbooks/collect-run-data.yaml + dependencies: + - name: charm-build-cinder-k8s + soft: true + - name: charm-build-cinder-ceph-k8s + soft: true + - name: charm-build-gnocchi-k8s + soft: true + - name: charm-build-ceilometer-k8s + soft: true + - name: charm-build-aodh-k8s + soft: true + - name: charm-build-keystone-k8s + soft: true + files: + - ops-sunbeam/ops_sunbeam/* + - charms/cinder-k8s/* + - charms/cinder-ceph-k8s/* + - charms/gnocchi-k8s/* + - charms/ceilometer-k8s/* + - charms/aodh-k8s/* + vars: + charm_jobs: + - charm-build-cinder-k8s + - charm-build-cinder-ceph-k8s + - charm-build-gnocchi-k8s + - charm-build-ceilometer-k8s + - charm-build-aodh-k8s + - charm-build-keystone-k8s + test_dir: tests/ceph +- job: + name: func-test-caas + description: | + Zaza smoke test for magnum and dependent charms + like heat, octavia, barbican. + timeout: 3600 + run: playbooks/zaza-func-test.yaml + post-run: playbooks/collect-run-data.yaml + dependencies: + - name: charm-build-heat-k8s + soft: true + - name: charm-build-octavia-k8s + soft: true + - name: charm-build-barbican-k8s + soft: true + - name: charm-build-magnum-k8s + soft: true + - name: charm-build-keystone-k8s + soft: true + - name: charm-build-glance-k8s + soft: true + - name: charm-build-ovn-central-k8s + soft: true + files: + - ops-sunbeam/ops_sunbeam/* + - charms/heat-k8s/* + - charms/octavia-k8s/* + - charms/barbican-k8s/* + - charms/magnum-k8s/* + vars: + charm_jobs: + - charm-build-heat-k8s + - charm-build-octavia-k8s + - charm-build-barbican-k8s + - charm-build-magnum-k8s + - charm-build-keystone-k8s + - charm-build-glance-k8s + - charm-build-ovn-central-k8s + test_dir: tests/caas +- job: + name: func-test-misc + description: | + Zaza smoke test for designate, desginate-bind, + keystone-ldap, openstack-exporter charms. + timeout: 3600 + run: playbooks/zaza-func-test.yaml + post-run: playbooks/collect-run-data.yaml + dependencies: + - name: charm-build-designate-k8s + soft: true + - name: charm-build-designate-bind-k8s + soft: true + - name: charm-build-keystone-k8s + soft: true + - name: charm-build-keystone-ldap-k8s + soft: true + - name: charm-build-openstack-exporter-k8s + soft: true + files: + - ops-sunbeam/ops_sunbeam/* + - charms/designate-k8s/* + - charms/designate-bind-k8s/* + - charms/keystone-ldap-k8s/* + - charms/openstack-exporter-k8s/* + vars: + charm_jobs: + - charm-build-designate-k8s + - charm-build-designate-bind-k8s + - charm-build-keystone-ldap-k8s + - charm-build-openstack-exporter-k8s + - charm-build-keystone-k8s + test_dir: tests/misc + +- job: + name: publish-charms + description: | + Publish all the charms built in the gate + pipeline. + post-review: true + run: playbooks/charm/publish.yaml + secrets: + charmhub_token + timeout: 3600 diff --git a/zuul.d/project-templates.yaml b/zuul.d/project-templates.yaml new file mode 100644 index 00000000..5c22e69e --- /dev/null +++ b/zuul.d/project-templates.yaml @@ -0,0 +1,137 @@ +- project-template: + name: openstack-python3-sunbeam-jobs + # NOTE(hemanth): This template is used in openstack sunbeam charms since + # stable/2023.1. The stable/2023.1 and stable/2023.2 charm branches + # support py310 unit tests and main support py310, py311 tests. + description: | + Runs unit tests for an OpenStack Sunbeam project under the CPython + version 3 releases designated for testing the latest release. + check: + jobs: + - openstack-tox-pep8 + - openstack-tox-py310: + branches: + - stable/2023.1 + - stable/2023.2 + - main + - openstack-tox-py311: + branches: + - main + gate: + jobs: + - openstack-tox-pep8 + - openstack-tox-py310: + branches: + - stable/2023.1 + - stable/2023.2 + - main + - openstack-tox-py311: + branches: + - main + +- project-template: + name: openstack-sunbeam-charm-build-jobs + description: | + Build the charms in OpenStack Sunbeam project. + check: + fail-fast: true + jobs: + - charm-build-keystone-k8s: + nodeset: ubuntu-focal + - charm-build-glance-k8s: + nodeset: ubuntu-focal + - charm-build-nova-k8s: + nodeset: ubuntu-focal + - charm-build-placement-k8s: + nodeset: ubuntu-focal + - charm-build-neutron-k8s: + nodeset: ubuntu-focal + - charm-build-ovn-central-k8s: + nodeset: ubuntu-focal + - charm-build-ovn-relay-k8s: + nodeset: ubuntu-focal + - charm-build-cinder-k8s: + nodeset: ubuntu-focal + - charm-build-cinder-ceph-k8s: + nodeset: ubuntu-focal + - charm-build-horizon-k8s: + nodeset: ubuntu-focal + - charm-build-heat-k8s: + nodeset: ubuntu-focal + - charm-build-octavia-k8s: + nodeset: ubuntu-focal + - charm-build-aodh-k8s: + nodeset: ubuntu-focal + - charm-build-ceilometer-k8s: + nodeset: ubuntu-focal + - charm-build-gnocchi-k8s: + nodeset: ubuntu-focal + - charm-build-barbican-k8s: + nodeset: ubuntu-focal + - charm-build-magnum-k8s: + nodeset: ubuntu-focal + - charm-build-designate-k8s: + nodeset: ubuntu-focal + - charm-build-designate-bind-k8s: + nodeset: ubuntu-focal + - charm-build-keystone-ldap-k8s: + nodeset: ubuntu-focal + - charm-build-openstack-exporter-k8s: + nodeset: ubuntu-focal + - charm-build-openstack-hypervisor: + nodeset: ubuntu-focal + gate: + fail-fast: true + jobs: + - charm-build-keystone-k8s: + nodeset: ubuntu-focal + - charm-build-glance-k8s: + nodeset: ubuntu-focal + - charm-build-nova-k8s: + nodeset: ubuntu-focal + - charm-build-placement-k8s: + nodeset: ubuntu-focal + - charm-build-neutron-k8s: + nodeset: ubuntu-focal + - charm-build-ovn-central-k8s: + nodeset: ubuntu-focal + - charm-build-ovn-relay-k8s: + nodeset: ubuntu-focal + - charm-build-cinder-k8s: + nodeset: ubuntu-focal + - charm-build-cinder-ceph-k8s: + nodeset: ubuntu-focal + - charm-build-horizon-k8s: + nodeset: ubuntu-focal + - charm-build-heat-k8s: + nodeset: ubuntu-focal + - charm-build-octavia-k8s: + nodeset: ubuntu-focal + - charm-build-aodh-k8s: + nodeset: ubuntu-focal + - charm-build-ceilometer-k8s: + nodeset: ubuntu-focal + - charm-build-gnocchi-k8s: + nodeset: ubuntu-focal + - charm-build-barbican-k8s: + nodeset: ubuntu-focal + - charm-build-magnum-k8s: + nodeset: ubuntu-focal + - charm-build-designate-k8s: + nodeset: ubuntu-focal + - charm-build-designate-bind-k8s: + nodeset: ubuntu-focal + - charm-build-keystone-ldap-k8s: + nodeset: ubuntu-focal + - charm-build-openstack-exporter-k8s: + nodeset: ubuntu-focal + - charm-build-openstack-hypervisor: + nodeset: ubuntu-focal + +- project-template: + name: charm-publish-jobs + description: | + The set of publish jobs for the OpenStack Sunbeam Charms + promote: + jobs: + - publish-charms diff --git a/zuul.d/secrets.yaml b/zuul.d/secrets.yaml new file mode 100644 index 00000000..45abce9d --- /dev/null +++ b/zuul.d/secrets.yaml @@ -0,0 +1,65 @@ +- secret: + name: charmhub_token + data: + # Generated on 27-Nov-2023 with 90 days ttl + value: !encrypted/pkcs1-oaep + - oWjNK48hexdzzazFW8SA6M3otdZtNYV3cYABxa+q0NPog347A4jzdH0vhwI/wO8X0H6l0 + ohgH54pI7jjfFGuG46LTZWgWyw7YET/jI7r025Oz9yiDl5178Zk0foPrTWjRnF0rQdKA3 + V90Tmfn8xhHabrn2T5CFUrhvNnUQHxRMKy77jA/1hjmexC8E5NA2ypBIpPyOhwrOcyLZ8 + t6PsgiSRiSxr7d8eWZBeBAo3LU3KFTBGCsBWp/kzVQCzItRN7SbCf5i8JXHPUySnyRCYT + UzMeb2FtlAf98Ng4SyGXys1iQ4zKn75PDgWp4FeJyociDCDlnuAlGmh3GQ+MkXWAPl+vU + RYtqnvkvhNl2p3rRTzUUlrrttTpWhj4Cp41whOgoe6ZLPaRjboXizQClp8qbGGIdgb980 + aiTcFegNZl2RRersfgSwIqq+ys+SgPkQ1kFXpFDuLeCqL/OTEP3fBYAE4h3iIsmo0LSWH + /I9Oe8LQr1TQUEAlUNKMNIhlC8nL5uUOQXxHAmmIVQHNZYF1tObehMnFCWvFEWh3C5CGD + QgrT5zvTHUoH3GOfhDTEMH2ssft9rRJgcUieNlGvfMiypoJvQL51vfTEOHgUqBfqF6FCs + 6FP/eewHUFxx00HyQmRSspRbnq6u7iTcWsZFOrY/Szo62EhEOHgKqMedpEGbRo= + - YIPNXBTQ65iqKpDbI8fKSG4gwYi58ORqCIrHPOiyGjuQiHbwFeExzZYtuFxPB7EYRJPwJ + 0rrkBF434z7ySjovdyBwtB2svHLqcreB5RYIc4hWXzQN1QRUuSi+j79IQ27EAmkryY3OY + dR504mVOIBkLBKgS3ETiWvVLiOcHCiZD6mnq/MnLFg1xDrWb1aGQujBjeUDMDK1I7Qay8 + wY+3MGnrsmqZjmrdzN7loUp/TxR9rtXWFKmIXLyKGeadSOTIIh/6nVQJfM72NUWcYpDUh + 22EonzEZ0l9f+rWNeGx5ww8X2BRffSfeOPaJZSp3gMfljuA5o0HEAlTKTR0uA1iKp7iVR + BAW91yZPdIGk2oYA9ytfjx7LZCK6Xxax2dxtaHoqyBm9jeyrJ1p6IbiqRDp7lP3IsuCbe + g1iXItY6IhF8kthUFUTxQP/yf94KmQR8tY8cpEhXPhCEaQLitW67fIlkc9grGwIg644IY + Rqd5TCxB2FQ9N71aK9smcTMfQZbNXqH4n9Ix3KcAaknnKEuWCR5+A7bKXG2VIsofl3cc5 + QmvyBzACoSvIeJHB73YABlesCKxBuBZX2lQQv3GB/KFilxtRAw6oPpZY744F9k1MFNO/R + NuZQJ3yVCq3QkWpM66kE774zR+3ZlQ639zTWZOXeZsjzZHuBSzRhnlezCk35mA= + - N7LtSHy4KoQiGMTcdZGDdawEB2BdbDQZabzQu0NnkW5kYMQ/hFqoRwPjLTB1mC0f/uJYy + jIgdQNX8uIXn6eAAuyaID9CJb9QO6rQ6+ZAHtj5FIpiCIoykEW4XpIZrIGFYWlgCjn87f + UDQgX3s5JUU5PAGUzaw2EWL3RtHgmHX6/Syh+tTS77qA1jam9edf5TwQ1JbXMERsdnl+k + fjs+THAzU7kt3XuMm08juZBkEvKP8lS87hmmYJWWDdpyxhph6FIhJ3LTOhJmQmN9ThUOL + kDOh5SueP9FOXF+2ozrJkq+PpcJQswZLiftKS54WSeFsIASeneGH1ZpVaMUKrE0XfFo5e + RVkUZC2MWJw0K4rDZmtBD0DD3LXfN22l3Pq0CJJb2p0pFbJ/x8+Xp7KCmuPYt559oeyko + H0ZMsxYlVgVwPB+6mkhck7UYZlGKpMZlvE7PTRCMHQ2bLT+GMGdx6vcHYJP/DQDvsf/H+ + z1j0K5HfjlU6RM4GNrbT2xAxyCDsdFfsB0sevjLr3HAsTCzBw2AYf9VoNGlGCVFqt2Sru + CQD138fDI27KcwO+rVPoJsmrZEGMDhUZDhZVySZ80xTojDOMHF3RcYDcfnOOt22kQiZJD + 8bNIw3E1++l+tv9d3Ki14Bpwlkm2SPw4mMPGHb4xxo15TYu/MPIp0CD18K1jCM= + - FtedxbTxdhPCJSwkx94pTapAwR1wbSABS/RMsluoNzxL74sv+xyNCkHVhSSTijPCEs5GH + +xPK4TVV2h065j8B2AlYmj2QspZc2F0BmhVLFQXU1k0yeGvnE2aID1WXrXteh9m6dW8i7 + NHVcnuFuvZDL0/zAbMhn2kQ0SycyQ11z/ZPil2Q3RA9EMSCjI16QSJ2HrVg6UDiKmBTSI + c+J8mrT2Fw1ChlHQvea/x0BH/piFoFWGJYOLo63v4eCmpfvaZbkzOUqjgE6RA4w40FtTv + UAnIM6NidNoFnneOCYRxmem8PBYO6HbeCh11NoS69cfT47Jb7u/JoEL4cCqAdFZDUmVnd + zelgu4RG/2G2fQ90+Co3f7wJ10INlw3R22SUMf4CpotSyxiEpWDEmwn1M1GjjKKJiSq/X + WCY0ZHxkipcMdr6g0rgZ9a6Ousc10W+/5IKKLxgEvqdRX32U8tSvvOXC6zGGbxTjvSsbL + RvjbOYXt93caowz1uXtKrerqC545f6yiS9rDCOwMN0KQemRvP8J42Rdik/MgUsWuGhlZs + neCGIWHdNLfLSAInSHrYLw5KiSmBAfHuVjvVJsLXsw7fNzkl3PorxjJX3ypOe8xoT7o+V + xdoqdxgLxpR81OEs4Zsqx4tkSuVijUlPk3UUl224uqKsAOVjLKdbLLF0PIV4u8= + - iAneLfeh/CYMYf1ilfpDERn5KdPEgjYcQZRsFV1iw58Gvn8nQgXj7YB0iHZIT32Jn2JnO + +duKUh0aqHcrlf44cHBqQkq5EkbyR4prA0fHoO7Y8e16gAhkfmWhm2yr48XD0HAuSk6ID + PTvOw60T+aS3yafJzI7H91HqVVol1kM9zwwLCDIsx6OG5EFh6P98WYLNZH4isyWEXvgAj + ot3AR6kO//bXQLy1k+p4ECCVx3N9PtdoodehX2m9PUwx/VkXfNiJx53arsTPZe3mTcDCC + EZC/Lp73j2F68uQgwSyKQeQnrxaq0OwGbxzetarjheZLPH8ilB4AthnP29rn+y3Ydp37V + WVhV8O4zm+nbl7bsKF7BtadJy3TxR865wl8zLI7XtA+K7SZ4SpEHH0qiy8zRSlBLTTil6 + E0li2xMPkyO8QC4I2Sbvj7LqfpfbK40UVXdte/nu0hpOw5rBxh4wp8l8jGUq8XYliJqkv + g4dvFZXeHTTm9IKft5LYZUUUIW2FZyg6Ds1qHkA0WFBNK2hymabgmN27R7srk2y0A5UV9 + 17VAo1zx7TniLlc3P163eLBR4xaNNOVyVcmSF7UJWKwg/Al6mRLuteDJrZUlWfZ+EmMe0 + m9WG9y3RElsqvFabkIoUr0s95eZHv6IG+wo/4wpn7wzFJl05rsxlpeT/y8X/AI= + - TFcU1ReK6vwjV3JBQa3mufB8GObXLreHIf7Ziu+rZ1rAe9O1eymPjtS1YiCmny8Jo4zRy + EN/BLuWljfGrQKRwfP3FJa/oOcRq8m9l6uGNsu/qs4aSEPoqWhSum6FJKII/1WDpY1619 + ckbUsFxyXWYbgS0Z65S5XGFHi4zUkAm2F8askpE3fmZ18kiTxw8evh99nfQWhblXAqEuO + yIyR+FpAXnHnnBBXpOI1Q+h+rfPQ9wACOo8LQKkEhZMWVh0mgNjlOifOS4ufnUlp+b87P + whDyahW2ZbWt2E9Zt+S3PheL6EQYO4WOjM/IyExj2dlViv2oTMs+NhK29I4e7e/danDon + +HY5yEOhvl3ecJNXxQQlW2QZE9RQfJQ5xAGQkUMIdwlJqeC0E8KAtYmVylE+D3k7gzQaY + /vvlqAA7YzRmqF5s6DkTN/FyOnd2IIVCxJvkCsudWzvegX9FUpIUg1UFhHtEKfd1N/ab6 + DmlpWzg7SivW4Vk07BN9nR/ONa0+B7c5VmQ0POH2VZaIVbalG18DKkoVZqMxDRB+MS1Hf + XUzhH6kz9zTyUcDfXY76VDQFiriL2IzlDyCWk+xvXSuEIu8EEfilNr0l6GgBppvKogb2y + HxZtKfLQNQrBszndmq6QLc0+6nPa8M6egt7RbeRgxyrWqqM6m9MfPVDKjOmw78= diff --git a/zuul.d/zuul.yaml b/zuul.d/zuul.yaml new file mode 100644 index 00000000..4eabeb1c --- /dev/null +++ b/zuul.d/zuul.yaml @@ -0,0 +1,48 @@ +- project: + templates: + - openstack-python3-sunbeam-jobs + - openstack-cover-jobs + - openstack-sunbeam-charm-build-jobs + check: + jobs: + - func-test-core: + nodeset: ubuntu-focal + voting: false + - func-test-ceph: + nodeset: ubuntu-focal + voting: false + - func-test-caas: + nodeset: ubuntu-focal + voting: false + - func-test-misc: + nodeset: ubuntu-focal + voting: false + vars: + juju_channel: 3.2/stable + juju_classic_mode: false + microk8s_channel: 1.28-strict/stable + microk8s_classic_mode: false + charmcraft_channel: 2.0/stable + publish_channels: + keystone-k8s: 2023.2/edge + glance-k8s: 2023.2/edge + nova-k8s: 2023.2/edge + placement-k8s: 2023.2/edge + neutron-k8s: 2023.2/edge + ovn-central-k8s: 23.09/edge + ovn-relay-k8s: 23.09/edge + cinder-k8s: 2023.2/edge + cinder-ceph-k8s: 2023.2/edge + horizon-k8s: 2023.2/edge + heat-k8s: 2023.2/edge + octavia-k8s: 2023.2/edge + aodh-k8s: 2023.2/edge + ceilometer-k8s: 2023.2/edge + gnocchi-k8s: 2023.2/edge + barbican-k8s: 2023.2/edge + designate-k8s: 2023.2/edge + designate-bind-k8s: 9/edge + magnum-k8s: 2023.2/edge + keystone-ldap-k8s: 2023.2/edge + openstack-exporter-k8s: 2023.2/edge + openstack-hypervisor: 2023.2/edge