From df41decd82ea911f1e46655c8d342f76b6972ead Mon Sep 17 00:00:00 2001
From: Hemanth Nakkina <hemanth.nakkina@canonical.com>
Date: Fri, 4 Aug 2023 03:39:54 +0530
Subject: [PATCH] Initial charm

Initial charm generated by running
- git init
- charmcraft init
- cookie cutter code from charm-ops-sunbeam
---
 charms/gnocchi-k8s/.gitignore                 |   9 +
 charms/gnocchi-k8s/CONTRIBUTING.md            |  34 +++
 charms/gnocchi-k8s/LICENSE                    | 202 ++++++++++++++++++
 charms/gnocchi-k8s/README.md                  |  26 +++
 charms/gnocchi-k8s/actions.yaml               |   2 +
 charms/gnocchi-k8s/charmcraft.yaml            |  19 ++
 charms/gnocchi-k8s/config.yaml                |  27 +++
 charms/gnocchi-k8s/metadata.yaml              |  47 ++++
 charms/gnocchi-k8s/pyproject.toml             |  46 ++++
 charms/gnocchi-k8s/requirements.txt           |   8 +
 charms/gnocchi-k8s/src/charm.py               |  61 ++++++
 charms/gnocchi-k8s/src/templates/ceph.conf.j2 |  22 ++
 .../src/templates/parts/database-connection   |   3 +
 .../src/templates/parts/identity-data         |  10 +
 .../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 ++
 .../src/templates/wsgi-gnocchi-api.conf       |  27 +++
 .../src/templates/wsgi-template.conf.j2       |  27 +++
 charms/gnocchi-k8s/test-requirements.txt      |  17 ++
 .../tests/integration/test_charm.py           |  35 +++
 charms/gnocchi-k8s/tests/unit/test_charm.py   |  68 ++++++
 charms/gnocchi-k8s/tox.ini                    | 134 ++++++++++++
 25 files changed, 860 insertions(+)
 create mode 100644 charms/gnocchi-k8s/.gitignore
 create mode 100644 charms/gnocchi-k8s/CONTRIBUTING.md
 create mode 100644 charms/gnocchi-k8s/LICENSE
 create mode 100644 charms/gnocchi-k8s/README.md
 create mode 100644 charms/gnocchi-k8s/actions.yaml
 create mode 100644 charms/gnocchi-k8s/charmcraft.yaml
 create mode 100644 charms/gnocchi-k8s/config.yaml
 create mode 100644 charms/gnocchi-k8s/metadata.yaml
 create mode 100644 charms/gnocchi-k8s/pyproject.toml
 create mode 100644 charms/gnocchi-k8s/requirements.txt
 create mode 100755 charms/gnocchi-k8s/src/charm.py
 create mode 100644 charms/gnocchi-k8s/src/templates/ceph.conf.j2
 create mode 100644 charms/gnocchi-k8s/src/templates/parts/database-connection
 create mode 100644 charms/gnocchi-k8s/src/templates/parts/identity-data
 create mode 100644 charms/gnocchi-k8s/src/templates/parts/section-database
 create mode 100644 charms/gnocchi-k8s/src/templates/parts/section-federation
 create mode 100644 charms/gnocchi-k8s/src/templates/parts/section-identity
 create mode 100644 charms/gnocchi-k8s/src/templates/parts/section-middleware
 create mode 100644 charms/gnocchi-k8s/src/templates/parts/section-signing
 create mode 100644 charms/gnocchi-k8s/src/templates/wsgi-gnocchi-api.conf
 create mode 100644 charms/gnocchi-k8s/src/templates/wsgi-template.conf.j2
 create mode 100644 charms/gnocchi-k8s/test-requirements.txt
 create mode 100644 charms/gnocchi-k8s/tests/integration/test_charm.py
 create mode 100644 charms/gnocchi-k8s/tests/unit/test_charm.py
 create mode 100644 charms/gnocchi-k8s/tox.ini

diff --git a/charms/gnocchi-k8s/.gitignore b/charms/gnocchi-k8s/.gitignore
new file mode 100644
index 00000000..a26d707f
--- /dev/null
+++ b/charms/gnocchi-k8s/.gitignore
@@ -0,0 +1,9 @@
+venv/
+build/
+*.charm
+.tox/
+.coverage
+__pycache__/
+*.py[cod]
+.idea
+.vscode/
diff --git a/charms/gnocchi-k8s/CONTRIBUTING.md b/charms/gnocchi-k8s/CONTRIBUTING.md
new file mode 100644
index 00000000..20e88bcc
--- /dev/null
+++ b/charms/gnocchi-k8s/CONTRIBUTING.md
@@ -0,0 +1,34 @@
+# Contributing
+
+To make contributions to this charm, you'll need a working [development setup](https://juju.is/docs/sdk/dev-setup).
+
+You can create an environment for development with `tox`:
+
+```shell
+tox devenv -e integration
+source venv/bin/activate
+```
+
+## Testing
+
+This project uses `tox` for managing test environments. There are some pre-configured environments
+that can be used for linting and formatting code when you're preparing contributions to the charm:
+
+```shell
+tox run -e format        # update your code according to linting rules
+tox run -e lint          # code style
+tox run -e static        # static type checking
+tox run -e unit          # unit tests
+tox run -e integration   # integration tests
+tox                      # runs 'format', 'lint', 'static', and 'unit' environments
+```
+
+## Build the charm
+
+Build the charm in this git repository using:
+
+```shell
+charmcraft pack
+```
+
+<!-- You may want to include any contribution/style guidelines in this document>
diff --git a/charms/gnocchi-k8s/LICENSE b/charms/gnocchi-k8s/LICENSE
new file mode 100644
index 00000000..8d771e18
--- /dev/null
+++ b/charms/gnocchi-k8s/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 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.
diff --git a/charms/gnocchi-k8s/README.md b/charms/gnocchi-k8s/README.md
new file mode 100644
index 00000000..104b9d5e
--- /dev/null
+++ b/charms/gnocchi-k8s/README.md
@@ -0,0 +1,26 @@
+<!--
+Avoid using this README file for information that is maintained or published elsewhere, e.g.:
+
+* metadata.yaml > published on Charmhub
+* documentation > published on (or linked to from) Charmhub
+* detailed contribution guide > documentation or CONTRIBUTING.md
+
+Use links instead.
+-->
+
+# gnocchi-k8s
+
+Charmhub package name: operator-template
+More information: https://charmhub.io/gnocchi-k8s
+
+Describe your charm in one or two sentences.
+
+## Other resources
+
+<!-- If your charm is documented somewhere else other than Charmhub, provide a link separately. -->
+
+- [Read more](https://example.com)
+
+- [Contributing](CONTRIBUTING.md) <!-- or link to other contribution documentation -->
+
+- See the [Juju SDK documentation](https://juju.is/docs/sdk) for more information about developing and improving charms.
diff --git a/charms/gnocchi-k8s/actions.yaml b/charms/gnocchi-k8s/actions.yaml
new file mode 100644
index 00000000..88e6195d
--- /dev/null
+++ b/charms/gnocchi-k8s/actions.yaml
@@ -0,0 +1,2 @@
+# NOTE: no actions yet!
+{ }
diff --git a/charms/gnocchi-k8s/charmcraft.yaml b/charms/gnocchi-k8s/charmcraft.yaml
new file mode 100644
index 00000000..ab6239a8
--- /dev/null
+++ b/charms/gnocchi-k8s/charmcraft.yaml
@@ -0,0 +1,19 @@
+type: "charm"
+bases:
+  - build-on:
+    - name: "ubuntu"
+      channel: "22.04"
+    run-on:
+    - name: "ubuntu"
+      channel: "22.04"
+parts:
+  charm:
+    build-packages:
+      - git
+      - libffi-dev
+      - libssl-dev
+    charm-binary-python-packages:
+      - cryptography
+      - jsonschema
+      - jinja2
+      - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
diff --git a/charms/gnocchi-k8s/config.yaml b/charms/gnocchi-k8s/config.yaml
new file mode 100644
index 00000000..606b5357
--- /dev/null
+++ b/charms/gnocchi-k8s/config.yaml
@@ -0,0 +1,27 @@
+options:
+  debug:
+    default: False
+    description: Enable debug logging.
+    type: boolean
+  os-admin-hostname:
+    default: glance.juju
+    description: |
+      The hostname or address of the admin endpoints that should be advertised
+      in the glance image provider.
+    type: string
+  os-internal-hostname:
+    default: glance.juju
+    description: |
+      The hostname or address of the internal endpoints that should be advertised
+      in the glance image provider.
+    type: string
+  os-public-hostname:
+    default: glance.juju
+    description: |
+      The hostname or address of the internal endpoints that should be advertised
+      in the glance image provider.
+    type: string
+  region:
+    default: RegionOne
+    description: Space delimited list of OpenStack regions
+    type: string
diff --git a/charms/gnocchi-k8s/metadata.yaml b/charms/gnocchi-k8s/metadata.yaml
new file mode 100644
index 00000000..34b5cd74
--- /dev/null
+++ b/charms/gnocchi-k8s/metadata.yaml
@@ -0,0 +1,47 @@
+name: gnocchi-k8s
+summary: OpenStack gnocchi service
+maintainer: OpenStack Charmers <openstack-charmers@lists.ubuntu.com>
+description: |
+  OpenStack gnocchi provides an HTTP service for managing, selecting,
+  and claiming providers of classes of inventory representing available
+  resources in a cloud.
+  .
+version: 3
+bases:
+  - name: ubuntu
+    channel: 20.04/stable
+tags:
+- openstack
+
+containers:
+  gnocchi-api:
+    resource: gnocchi-api-image
+
+resources:
+  gnocchi-api-image:
+    type: oci-image
+    description: OCI image for OpenStack gnocchi
+
+requires:
+  database:
+    interface: mysql_client
+    limit: 1
+  identity-service:
+    interface: keystone
+  ingress-internal:
+    interface: ingress
+    optional: true
+    limit: 1
+  ingress-public:
+    interface: ingress
+    limit: 1
+  amqp:
+    interface: rabbitmq
+
+provides:
+  gnocchi:
+    interface: gnocchi
+
+peers:
+  peers:
+    interface: gnocchi-peer
diff --git a/charms/gnocchi-k8s/pyproject.toml b/charms/gnocchi-k8s/pyproject.toml
new file mode 100644
index 00000000..e10531c7
--- /dev/null
+++ b/charms/gnocchi-k8s/pyproject.toml
@@ -0,0 +1,46 @@
+# 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"]
+
+# Linting tools configuration
+[tool.ruff]
+line-length = 99
+select = ["E", "W", "F", "C", "N", "D", "I001"]
+extend-ignore = [
+    "D203",
+    "D204",
+    "D213",
+    "D215",
+    "D400",
+    "D404",
+    "D406",
+    "D407",
+    "D408",
+    "D409",
+    "D413",
+]
+ignore = ["E501", "D107"]
+extend-exclude = ["__pycache__", "*.egg_info"]
+per-file-ignores = {"tests/*" = ["D100","D101","D102","D103","D104"]}
+
+[tool.ruff.mccabe]
+max-complexity = 10
+
+[tool.codespell]
+skip = "build,lib,venv,icon.svg,.tox,.git,.mypy_cache,.ruff_cache,.coverage"
+
+[tool.pyright]
+include = ["src/**.py"]
+
diff --git a/charms/gnocchi-k8s/requirements.txt b/charms/gnocchi-k8s/requirements.txt
new file mode 100644
index 00000000..99f36564
--- /dev/null
+++ b/charms/gnocchi-k8s/requirements.txt
@@ -0,0 +1,8 @@
+ops
+jinja2
+git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam
+lightkube
+# These are only needeed if the charm relates to ceph
+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
diff --git a/charms/gnocchi-k8s/src/charm.py b/charms/gnocchi-k8s/src/charm.py
new file mode 100755
index 00000000..a6f5b8a4
--- /dev/null
+++ b/charms/gnocchi-k8s/src/charm.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python3
+"""Gnocchi Operator Charm.
+
+This charm provide Gnocchi services as part of an OpenStack deployment
+"""
+
+import logging
+
+from ops.framework import StoredState
+from ops.main import main
+
+import ops_sunbeam.charm as sunbeam_charm
+
+logger = logging.getLogger(__name__)
+
+
+class GnocchiOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
+    """Charm the service."""
+
+    _state = StoredState()
+    service_name = "gnocchi-api"
+    wsgi_admin_script = '/usr/bin/gnocchi-api-wsgi'
+    wsgi_public_script = '/usr/bin/gnocchi-api-wsgi'
+
+    db_sync_cmds = [
+        ['/snap/bin/gnocchi.upgrade', '--log-file=/var/snap/gnocchi/common/log/gnocchi-upgrade.log']
+    ]
+
+    @property
+    def service_conf(self) -> str:
+        """Service default configuration file."""
+        return f"/etc/gnocchi/gnocchi.conf"
+
+    @property
+    def service_user(self) -> str:
+        """Service user file and directory ownership."""
+        return 'gnocchi'
+
+    @property
+    def service_group(self) -> str:
+        """Service group file and directory ownership."""
+        return 'gnocchi'
+
+    @property
+    def service_endpoints(self):
+        return [
+            {
+                'service_name': 'gnocchi',
+                'type': 'gnocchi',
+                'description': "OpenStack Gnocchi API",
+                'internal_url': f'{self.internal_url}',
+                'public_url': f'{self.public_url}',
+                'admin_url': f'{self.admin_url}'}]
+
+    @property
+    def default_public_ingress_port(self):
+        return 8041
+
+
+if __name__ == "__main__":
+    main(GnocchiOperatorCharm)
diff --git a/charms/gnocchi-k8s/src/templates/ceph.conf.j2 b/charms/gnocchi-k8s/src/templates/ceph.conf.j2
new file mode 100644
index 00000000..c293ae90
--- /dev/null
+++ b/charms/gnocchi-k8s/src/templates/ceph.conf.j2
@@ -0,0 +1,22 @@
+###############################################################################
+# [ WARNING ]
+# ceph configuration file maintained in aso
+# local changes may be overwritten.
+###############################################################################
+[global]
+{% if ceph.auth -%}
+auth_supported = {{ ceph.auth }}
+mon host = {{ ceph.mon_hosts }}
+{% endif -%}
+keyring = /etc/ceph/$cluster.$name.keyring
+log to syslog = false
+err to syslog = false
+clog to syslog = false
+{% if ceph.rbd_features %}
+rbd default features = {{ ceph.rbd_features }}
+{% endif %}
+
+[client]
+{% if ceph_config.rbd_default_data_pool -%}
+rbd default data pool = {{ ceph_config.rbd_default_data_pool }}
+{% endif %}
diff --git a/charms/gnocchi-k8s/src/templates/parts/database-connection b/charms/gnocchi-k8s/src/templates/parts/database-connection
new file mode 100644
index 00000000..1fd70ce2
--- /dev/null
+++ b/charms/gnocchi-k8s/src/templates/parts/database-connection
@@ -0,0 +1,3 @@
+{% 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
new file mode 100644
index 00000000..4b4af021
--- /dev/null
+++ b/charms/gnocchi-k8s/src/templates/parts/identity-data
@@ -0,0 +1,10 @@
+{% if identity_service.internal_host -%}
+www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
+auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
+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 }}
+{% endif -%}
diff --git a/charms/gnocchi-k8s/src/templates/parts/section-database b/charms/gnocchi-k8s/src/templates/parts/section-database
new file mode 100644
index 00000000..986d9b10
--- /dev/null
+++ b/charms/gnocchi-k8s/src/templates/parts/section-database
@@ -0,0 +1,3 @@
+[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
new file mode 100644
index 00000000..65ee99ed
--- /dev/null
+++ b/charms/gnocchi-k8s/src/templates/parts/section-federation
@@ -0,0 +1,10 @@
+{% 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
new file mode 100644
index 00000000..7568a9a4
--- /dev/null
+++ b/charms/gnocchi-k8s/src/templates/parts/section-identity
@@ -0,0 +1,2 @@
+[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
new file mode 100644
index 00000000..e65f1d98
--- /dev/null
+++ b/charms/gnocchi-k8s/src/templates/parts/section-middleware
@@ -0,0 +1,6 @@
+{% 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
new file mode 100644
index 00000000..cb7d69ae
--- /dev/null
+++ b/charms/gnocchi-k8s/src/templates/parts/section-signing
@@ -0,0 +1,15 @@
+{% 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/src/templates/wsgi-gnocchi-api.conf b/charms/gnocchi-k8s/src/templates/wsgi-gnocchi-api.conf
new file mode 100644
index 00000000..b34c076e
--- /dev/null
+++ b/charms/gnocchi-k8s/src/templates/wsgi-gnocchi-api.conf
@@ -0,0 +1,27 @@
+Listen {{ wsgi_config.public_port }}
+<VirtualHost *:{{ wsgi_config.public_port }}>
+    WSGIDaemonProcess {{ wsgi_config.group }} processes=3 threads=1 user={{ wsgi_config.user }} group={{ wsgi_config.group }} \
+                      display-name=%{GROUP}
+    WSGIProcessGroup {{ wsgi_config.group }}
+    {% if ingress_internal.ingress_path -%}
+    WSGIScriptAlias {{ ingress_internal.ingress_path }} {{ wsgi_config.wsgi_public_script }}
+    {% endif -%}
+    WSGIScriptAlias / {{ wsgi_config.wsgi_public_script }}
+    WSGIApplicationGroup %{GLOBAL}
+    WSGIPassAuthorization On
+    <IfVersion >= 2.4>
+      ErrorLogFormat "%{cu}t %M"
+    </IfVersion>
+    ErrorLog {{ wsgi_config.error_log }}
+    CustomLog {{ wsgi_config.custom_log }} combined
+
+    <Directory /usr/bin>
+        <IfVersion >= 2.4>
+            Require all granted
+        </IfVersion>
+        <IfVersion < 2.4>
+            Order allow,deny
+            Allow from all
+        </IfVersion>
+    </Directory>
+</VirtualHost>
diff --git a/charms/gnocchi-k8s/src/templates/wsgi-template.conf.j2 b/charms/gnocchi-k8s/src/templates/wsgi-template.conf.j2
new file mode 100644
index 00000000..b34c076e
--- /dev/null
+++ b/charms/gnocchi-k8s/src/templates/wsgi-template.conf.j2
@@ -0,0 +1,27 @@
+Listen {{ wsgi_config.public_port }}
+<VirtualHost *:{{ wsgi_config.public_port }}>
+    WSGIDaemonProcess {{ wsgi_config.group }} processes=3 threads=1 user={{ wsgi_config.user }} group={{ wsgi_config.group }} \
+                      display-name=%{GROUP}
+    WSGIProcessGroup {{ wsgi_config.group }}
+    {% if ingress_internal.ingress_path -%}
+    WSGIScriptAlias {{ ingress_internal.ingress_path }} {{ wsgi_config.wsgi_public_script }}
+    {% endif -%}
+    WSGIScriptAlias / {{ wsgi_config.wsgi_public_script }}
+    WSGIApplicationGroup %{GLOBAL}
+    WSGIPassAuthorization On
+    <IfVersion >= 2.4>
+      ErrorLogFormat "%{cu}t %M"
+    </IfVersion>
+    ErrorLog {{ wsgi_config.error_log }}
+    CustomLog {{ wsgi_config.custom_log }} combined
+
+    <Directory /usr/bin>
+        <IfVersion >= 2.4>
+            Require all granted
+        </IfVersion>
+        <IfVersion < 2.4>
+            Order allow,deny
+            Allow from all
+        </IfVersion>
+    </Directory>
+</VirtualHost>
diff --git a/charms/gnocchi-k8s/test-requirements.txt b/charms/gnocchi-k8s/test-requirements.txt
new file mode 100644
index 00000000..8057d2c6
--- /dev/null
+++ b/charms/gnocchi-k8s/test-requirements.txt
@@ -0,0 +1,17 @@
+# 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.
+charm-tools>=2.4.4
+coverage>=3.6
+mock>=1.2
+flake8>=2.2.4,<=2.4.1
+pyflakes==2.1.1
+stestr>=2.2.0
+requests>=2.18.4
+psutil
+# oslo.i18n dropped py35 support
+oslo.i18n<4.0.0
+git+https://github.com/openstack-charmers/zaza.git#egg=zaza
+git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
+pytz    # workaround for 14.04 pip/tox
+pyudev  # for ceph-* charm unit tests (not mocked?)
diff --git a/charms/gnocchi-k8s/tests/integration/test_charm.py b/charms/gnocchi-k8s/tests/integration/test_charm.py
new file mode 100644
index 00000000..6823bbd9
--- /dev/null
+++ b/charms/gnocchi-k8s/tests/integration/test_charm.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+# Copyright 2023 Canonical Ltd.
+# See LICENSE file for licensing details.
+
+import asyncio
+import logging
+from pathlib import Path
+
+import pytest
+import yaml
+from pytest_operator.plugin import OpsTest
+
+logger = logging.getLogger(__name__)
+
+METADATA = yaml.safe_load(Path("./metadata.yaml").read_text())
+APP_NAME = METADATA["name"]
+
+
+@pytest.mark.abort_on_fail
+async def test_build_and_deploy(ops_test: OpsTest):
+    """Build the charm-under-test and deploy it together with related charms.
+
+    Assert on the unit status before any relations/configurations take place.
+    """
+    # Build and deploy charm from local source folder
+    charm = await ops_test.build_charm(".")
+    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(charm, resources=resources, application_name=APP_NAME),
+        ops_test.model.wait_for_idle(
+            apps=[APP_NAME], status="active", raise_on_blocked=True, timeout=1000
+        ),
+    )
diff --git a/charms/gnocchi-k8s/tests/unit/test_charm.py b/charms/gnocchi-k8s/tests/unit/test_charm.py
new file mode 100644
index 00000000..ba8a0efd
--- /dev/null
+++ b/charms/gnocchi-k8s/tests/unit/test_charm.py
@@ -0,0 +1,68 @@
+# Copyright 2023 Canonical Ltd.
+# See LICENSE file for licensing details.
+#
+# Learn more about testing at: https://juju.is/docs/sdk/testing
+
+import unittest
+
+import ops
+import ops.testing
+from charm import GnocchiK8SCharm
+
+
+class TestCharm(unittest.TestCase):
+    def setUp(self):
+        self.harness = ops.testing.Harness(GnocchiK8SCharm)
+        self.addCleanup(self.harness.cleanup)
+        self.harness.begin()
+
+    def test_httpbin_pebble_ready(self):
+        # Expected plan after Pebble ready with default config
+        expected_plan = {
+            "services": {
+                "httpbin": {
+                    "override": "replace",
+                    "summary": "httpbin",
+                    "command": "gunicorn -b 0.0.0.0:80 httpbin:app -k gevent",
+                    "startup": "enabled",
+                    "environment": {"GUNICORN_CMD_ARGS": "--log-level info"},
+                }
+            },
+        }
+        # Simulate the container coming up and emission of pebble-ready event
+        self.harness.container_pebble_ready("httpbin")
+        # Get the plan now we've run PebbleReady
+        updated_plan = self.harness.get_container_pebble_plan("httpbin").to_dict()
+        # Check we've got the plan we expected
+        self.assertEqual(expected_plan, updated_plan)
+        # Check the service was started
+        service = self.harness.model.unit.get_container("httpbin").get_service("httpbin")
+        self.assertTrue(service.is_running())
+        # Ensure we set an ActiveStatus with no message
+        self.assertEqual(self.harness.model.unit.status, ops.ActiveStatus())
+
+    def test_config_changed_valid_can_connect(self):
+        # Ensure the simulated Pebble API is reachable
+        self.harness.set_can_connect("httpbin", True)
+        # Trigger a config-changed event with an updated value
+        self.harness.update_config({"log-level": "debug"})
+        # Get the plan now we've run PebbleReady
+        updated_plan = self.harness.get_container_pebble_plan("httpbin").to_dict()
+        updated_env = updated_plan["services"]["httpbin"]["environment"]
+        # Check the config change was effective
+        self.assertEqual(updated_env, {"GUNICORN_CMD_ARGS": "--log-level debug"})
+        self.assertEqual(self.harness.model.unit.status, ops.ActiveStatus())
+
+    def test_config_changed_valid_cannot_connect(self):
+        # Trigger a config-changed event with an updated value
+        self.harness.update_config({"log-level": "debug"})
+        # Check the charm is in WaitingStatus
+        self.assertIsInstance(self.harness.model.unit.status, ops.WaitingStatus)
+
+    def test_config_changed_invalid(self):
+        # Ensure the simulated Pebble API is reachable
+        self.harness.set_can_connect("httpbin", True)
+        # Trigger a config-changed event with an updated value
+        self.harness.update_config({"log-level": "foobar"})
+        # Check the charm is in BlockedStatus
+        self.assertIsInstance(self.harness.model.unit.status, ops.BlockedStatus)
diff --git a/charms/gnocchi-k8s/tox.ini b/charms/gnocchi-k8s/tox.ini
new file mode 100644
index 00000000..31301b80
--- /dev/null
+++ b/charms/gnocchi-k8s/tox.ini
@@ -0,0 +1,134 @@
+# Operator charm (with zaza): tox.ini
+
+[tox]
+envlist = pep8,py3
+skipsdist = True
+# NOTE: Avoid build/test env pollution by not enabling sitepackages.
+sitepackages = False
+# NOTE: Avoid false positives by not skipping missing interpreters.
+skip_missing_interpreters = False
+# NOTES:
+# * We avoid the new dependency resolver by pinning pip < 20.3, see
+#   https://github.com/pypa/pip/issues/9187
+# * Pinning dependencies requires tox >= 3.2.0, see
+#   https://tox.readthedocs.io/en/latest/config.html#conf-requires
+# * It is also necessary to pin virtualenv as a newer virtualenv would still
+#   lead to fetching the latest pip in the func* tox targets, see
+#   https://stackoverflow.com/a/38133283
+requires = pip < 20.3
+           virtualenv < 20.0
+# NOTE: https://wiki.canonical.com/engineering/OpenStack/InstallLatestToxOnOsci
+minversion = 3.2.0
+
+[testenv]
+setenv = VIRTUAL_ENV={envdir}
+         PYTHONHASHSEED=0
+         CHARM_DIR={envdir}
+install_command =
+  pip install {opts} {packages}
+commands = stestr run --slowest {posargs}
+whitelist_externals =
+    git
+    add-to-archive.py
+    bash
+    charmcraft
+passenv = HOME TERM CS_* OS_* TEST_*
+deps = -r{toxinidir}/test-requirements.txt
+
+[testenv:py35]
+basepython = python3.5
+# python3.5 is irrelevant on a focal+ charm.
+commands = /bin/true
+
+[testenv:py36]
+basepython = python3.6
+deps = -r{toxinidir}/requirements.txt
+       -r{toxinidir}/test-requirements.txt
+
+[testenv:py37]
+basepython = python3.7
+deps = -r{toxinidir}/requirements.txt
+       -r{toxinidir}/test-requirements.txt
+
+[testenv:py38]
+basepython = python3.8
+deps = -r{toxinidir}/requirements.txt
+       -r{toxinidir}/test-requirements.txt
+
+[testenv:py3]
+basepython = python3
+deps = -r{toxinidir}/requirements.txt
+       -r{toxinidir}/test-requirements.txt
+
+[testenv:pep8]
+basepython = python3
+deps = -r{toxinidir}/requirements.txt
+       -r{toxinidir}/test-requirements.txt
+commands = flake8 {posargs} src unit_tests tests
+
+[testenv:cover]
+# Technique based heavily upon
+# https://github.com/openstack/nova/blob/master/tox.ini
+basepython = python3
+deps = -r{toxinidir}/requirements.txt
+       -r{toxinidir}/test-requirements.txt
+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
+
+[coverage:run]
+branch = True
+concurrency = multiprocessing
+parallel = True
+source =
+    .
+omit =
+    .tox/*
+    */charmhelpers/*
+    unit_tests/*
+
+[testenv:venv]
+basepython = python3
+commands = {posargs}
+
+[testenv:build]
+basepython = python3
+deps = -r{toxinidir}/build-requirements.txt
+commands =
+    charmcraft build
+
+[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
+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}
+
+[flake8]
+# Ignore E902 because the unit_tests directory is missing in the built charm.
+ignore = E402,E226,E902