From f07ada88f18a44a2c474ff08cf872913cb19aa79 Mon Sep 17 00:00:00 2001 From: Claudiu Belu Date: Fri, 29 Aug 2025 15:35:08 +0000 Subject: [PATCH] Adds ironic-k8s charm Adds the basic charm, with typical relations and setup. The charm has the following resources: - ironic-api-image - ironic-novncproxy-image The charm enables the audit middleware for the Ironic API. The charm provides the "ironic-api" relation, based on service_readiness. The charm has the traefik-route-internal and traefik-route-public for the ironic-novncproxy service. Change-Id: Ic00a933bae43a5d598c03eef18969a41138ea653 Signed-off-by: Claudiu Belu --- charms/ironic-k8s/.gitignore | 1 + charms/ironic-k8s/.sunbeam-build.yaml | 21 + charms/ironic-k8s/CONTRIBUTING.md | 63 +++ charms/ironic-k8s/LICENSE | 202 +++++++++ charms/ironic-k8s/README.md | 85 ++++ charms/ironic-k8s/charmcraft.yaml | 119 +++++ charms/ironic-k8s/pyproject.toml | 17 + charms/ironic-k8s/rebuild | 3 + charms/ironic-k8s/src/charm.py | 411 ++++++++++++++++++ .../src/templates/api_audit_map.conf | 29 ++ .../ironic-k8s/src/templates/ironic.conf.j2 | 31 ++ charms/ironic-k8s/src/templates/rootwrap.conf | 27 ++ .../src/templates/wsgi-ironic-api.conf.j2 | 28 ++ charms/ironic-k8s/tests/unit/__init__.py | 16 + charms/ironic-k8s/tests/unit/test_charm.py | 154 +++++++ charms/ironic-k8s/uv.lock | 323 ++++++++++++++ tests/all-k8s/smoke.yaml.j2 | 27 ++ tests/all-k8s/tests.yaml | 3 + zuul.d/jobs.yaml | 29 ++ zuul.d/project-templates.yaml | 6 + zuul.d/zuul.yaml | 1 + 21 files changed, 1596 insertions(+) create mode 100644 charms/ironic-k8s/.gitignore create mode 100644 charms/ironic-k8s/.sunbeam-build.yaml create mode 100644 charms/ironic-k8s/CONTRIBUTING.md create mode 100644 charms/ironic-k8s/LICENSE create mode 100644 charms/ironic-k8s/README.md create mode 100644 charms/ironic-k8s/charmcraft.yaml create mode 100644 charms/ironic-k8s/pyproject.toml create mode 100644 charms/ironic-k8s/rebuild create mode 100755 charms/ironic-k8s/src/charm.py create mode 100644 charms/ironic-k8s/src/templates/api_audit_map.conf create mode 100644 charms/ironic-k8s/src/templates/ironic.conf.j2 create mode 100644 charms/ironic-k8s/src/templates/rootwrap.conf create mode 100644 charms/ironic-k8s/src/templates/wsgi-ironic-api.conf.j2 create mode 100644 charms/ironic-k8s/tests/unit/__init__.py create mode 100644 charms/ironic-k8s/tests/unit/test_charm.py create mode 100644 charms/ironic-k8s/uv.lock diff --git a/charms/ironic-k8s/.gitignore b/charms/ironic-k8s/.gitignore new file mode 100644 index 00000000..0bf1ce9c --- /dev/null +++ b/charms/ironic-k8s/.gitignore @@ -0,0 +1 @@ +!lib/charms/ironic_k8s diff --git a/charms/ironic-k8s/.sunbeam-build.yaml b/charms/ironic-k8s/.sunbeam-build.yaml new file mode 100644 index 00000000..2d5db57b --- /dev/null +++ b/charms/ironic-k8s/.sunbeam-build.yaml @@ -0,0 +1,21 @@ +external-libraries: + - charms.data_platform_libs.v0.data_interfaces + - charms.rabbitmq_k8s.v0.rabbitmq + - charms.traefik_k8s.v2.ingress + - charms.traefik_route_k8s.v0.traefik_route + - charms.certificate_transfer_interface.v0.certificate_transfer + - charms.loki_k8s.v1.loki_push_api + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing +internal-libraries: + - charms.keystone_k8s.v1.identity_service + - charms.sunbeam_libs.v0.service_readiness +templates: + - parts/section-database + - parts/database-connection + - parts/database-connection-settings + - parts/section-identity + - parts/identity-data + - parts/section-oslo-messaging-rabbit + - parts/section-service-user + - ca-bundle.pem.j2 diff --git a/charms/ironic-k8s/CONTRIBUTING.md b/charms/ironic-k8s/CONTRIBUTING.md new file mode 100644 index 00000000..27fb7ac5 --- /dev/null +++ b/charms/ironic-k8s/CONTRIBUTING.md @@ -0,0 +1,63 @@ +# ironic-k8s + +## Developing + +Create and activate a virtualenv with the development requirements: + + virtualenv -p python3 venv + source venv/bin/activate + pip install -r requirements-dev.txt + +## Code overview + +Get familiarised with [Charmed Operator Framework](https://juju.is/docs/sdk) +and [Sunbeam documentation](sunbeam-docs). + +ironic-k8s charm uses the ops\_sunbeam library and extends +OSBaseOperatorAPICharm from the library. + +ironic-k8s charm consumes database relation to connect to database, +identity-service to register the service endpoints to keystone +and ingress-internal/ingress-public relation to get exposed over +internal and public networks. + +The charms starts the ironic-api and ironic-novncproxy services. + +## Intended use case + +ironic-k8s charm deploys and configures OpenStack Ironic - a bare metal +provisioning service - on a kubernetes based environment. + +## Roadmap + +TODO + +## Testing + +The Python operator framework includes a very nice harness for testing +operator behaviour without full deployment. Run tests using command: + + tox --root ../../ -e py3 -- ironic-k8s + +## Deployment + +This project uses tox for building and managing. To build the charm +run: + + tox --root ../../ -e build -- ironic-k8s + +To deploy the local test instance: + + juju deploy ./ironic-k8s.charm ironic --trust \ + --resource ironic-api-image=ghcr.io/canonical/ironic-api:2025.1 \ + --resource ironic-novncproxy-image=ghcr.io/canonical/ironic-novncproxy:2025.1 + +To upgrade / refresh the ironic-k8s charm with a locally-built charm, use the +following command: + + juju refresh ironic --path ./ironic-k8s.charm + + + + +[sunbeam-docs]: https://opendev.org/openstack/sunbeam-charms/src/branch/main/README.md diff --git a/charms/ironic-k8s/LICENSE b/charms/ironic-k8s/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/charms/ironic-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 [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/charms/ironic-k8s/README.md b/charms/ironic-k8s/README.md new file mode 100644 index 00000000..4fbaf8c2 --- /dev/null +++ b/charms/ironic-k8s/README.md @@ -0,0 +1,85 @@ +# ironic-k8s + +## Description + +ironic-k8s is an operator to manage the Ironic API and noVNC proxy +services on a Kubernetes based environment. + +## Usage + +### Deployment + +ironic-k8s is deployed using command below: + + juju deploy ironic-k8s ironic --trust + +For instructions on how to build the charm and deploy / refresh it, check out +the [CONTRIBUTING.md][contributors-guide]. + +Now connect the ironic operator to existing database, messaging, +keystone identity, and traefik operators: + + juju deploy mysql-router-k8s ironic-mysql-router --trust + juju relate ironic-mysql-router:backend-database mysql:database + juju relate ironic-mysql-router:database ironic:database + juju relate rabbitmq:amqp ironic:amqp + juju relate keystone:identity-service ironic:identity-service + juju relate traefik:ingress ironic:ingress-internal + juju relate traefik:traefik-route ironic:traefik-route-internal + juju relate traefik-public:traefik-route ironic:traefik-route-public + +### Configuration + +This section covers common and/or important configuration options. See file +`config.yaml` for the full list of options, along with their descriptions and +default values. See the [Juju documentation][juju-docs-config-apps] for details +on configuring applications. + +### Actions + +This section covers Juju [actions][juju-docs-actions] supported by the charm. +Actions allow specific operations to be performed on a per-unit basis. To +display action descriptions run `juju actions ironic`. If the charm is not +deployed then see file `actions.yaml`. + +## Relations + +ironic-k8s requires the following relations: + +- `amqp`: To connect to RabbitMQ. +- `database`: To connect to MySQL. +- `identity-service`: To register endpoints in Keystone. +- `ingress-internal`: To expose service on underlying internal network. +- `traefik-route-internal`: To create an internal Traefik route for `ironic-novncproxy`. + +The following relations are optional: + +- `ingress-public`: To expose service on public network. +- `logging`: To send logs to Loki. +- `receive-ca-cert`: To enable TLS on the service endpoints. +- `tracing`: To connect to a tracing backend. +- `traefik-route-public`: To create a internal Traefik route for `ironic-novncproxy`. + +## OCI Images + +The charm by default uses following images: + +- `ghcr.io/canonical/ironic-api:2025.1` +- `ghcr.io/canonical/ironic-novncproxy:2025.1` + +## Contributing + +Please see the [Juju SDK docs](https://juju.is/docs/sdk) for guidelines +on enhancements to this charm following best practice guidelines, and +[CONTRIBUTING.md][contributors-guide] for developer guidance. + +## Bugs + +Please report bugs on [Launchpad][lp-bugs-charm-ironic-k8s]. + + + +[contributors-guide]: https://opendev.org/openstack/sunbeam-charms/src/branch/main/charms/ironic-k8s/CONTRIBUTING.md +[juju-docs-actions]: https://jaas.ai/docs/actions +[juju-docs-config-apps]: https://documentation.ubuntu.com/juju/3.6/reference/configuration/#application-configuration +[lp-bugs-charm-ironic-k8s]: https://bugs.launchpad.net/sunbeam-charms/+filebug diff --git a/charms/ironic-k8s/charmcraft.yaml b/charms/ironic-k8s/charmcraft.yaml new file mode 100644 index 00000000..15e6023d --- /dev/null +++ b/charms/ironic-k8s/charmcraft.yaml @@ -0,0 +1,119 @@ +type: charm +title: OpenStack Ironic service +name: ironic-k8s +summary: OpenStack bare metal provisioning service +description: | + ironic-k8s is a charm for the OpenStack Ironic service, a bare metal + provisioning service. +assumes: + - k8s-api + - juju >= 3.1 +links: + source: https://opendev.org/openstack/sunbeam-charms + issues: https://bugs.launchpad.net/sunbeam-charms + +base: ubuntu@24.04 +platforms: + amd64: + +config: + options: + debug: + default: false + description: Enable debug logging + type: boolean + region: + default: RegionOne + description: Name of the OpenStack region + type: string + +containers: + ironic-api: + resource: ironic-api-image + ironic-novncproxy: + resource: ironic-novncproxy-image + +resources: + ironic-api-image: + type: oci-image + description: OCI image for OpenStack Ironic API + upstream-source: ghcr.io/canonical/ironic-api:2025.1 + ironic-novncproxy-image: + type: oci-image + description: OCI image for OpenStack Ironic noVNC proxy + upstream-source: ghcr.io/canonical/ironic-novncproxy:2025.1 + +requires: + amqp: + interface: rabbitmq + limit: 1 + database: + interface: mysql_client + limit: 1 + identity-service: + interface: keystone + limit: 1 + ingress-internal: + interface: ingress + limit: 1 + ingress-public: + interface: ingress + optional: true + limit: 1 + logging: + interface: loki_push_api + optional: true + limit: 1 + receive-ca-cert: + interface: certificate_transfer + optional: true + limit: 1 + tracing: + interface: tracing + optional: true + limit: 1 + # For ironic-novncproxy + traefik-route-internal: + interface: traefik_route + limit: 1 + # For ironic-novncproxy + traefik-route-public: + interface: traefik_route + optional: true + limit: 1 + +provides: + ironic-api: + interface: baremetal + +peers: + peers: + interface: ironic-peer + +parts: + update-certificates: + plugin: nil + override-build: | + apt update + apt install -y ca-certificates + update-ca-certificates + charm: + after: + - update-certificates + build-packages: + - git + - libffi-dev + - libssl-dev + - rustc-1.80 + - cargo-1.80 + - pkg-config + charm-binary-python-packages: + - cryptography + - jsonschema + - pydantic + - jinja2 + build-snaps: [astral-uv] + override-build: | + uv export --frozen --no-hashes --format=requirements-txt -o requirements.txt + craftctl default + charm-requirements: [requirements.txt] diff --git a/charms/ironic-k8s/pyproject.toml b/charms/ironic-k8s/pyproject.toml new file mode 100644 index 00000000..a3668d55 --- /dev/null +++ b/charms/ironic-k8s/pyproject.toml @@ -0,0 +1,17 @@ +# Copyright 2025 Canonical Ltd. +# See LICENSE file for licensing details. + +[project] +name = "ironic-k8s" +version = "2025.1" +requires-python = "~=3.12.0" + +dependencies = [ + "jinja2", + "jsonschema", + "pydantic", + "ops", + "interface_tls_certificates@git+https://opendev.org/openstack/charm-ops-interface-tls-certificates", + "tenacity", # From ops_sunbeam + "opentelemetry-api~=1.21.0", # charm_tracing library -> opentelemetry-sdk requires 1.21.0 +] diff --git a/charms/ironic-k8s/rebuild b/charms/ironic-k8s/rebuild new file mode 100644 index 00000000..1d2e03a6 --- /dev/null +++ b/charms/ironic-k8s/rebuild @@ -0,0 +1,3 @@ +# This file is used to trigger a build. +# Change uuid to trigger a new build. +842dcec3-813e-41d1-8d6e-c0ea6b558a2b diff --git a/charms/ironic-k8s/src/charm.py b/charms/ironic-k8s/src/charm.py new file mode 100755 index 00000000..a91a21ae --- /dev/null +++ b/charms/ironic-k8s/src/charm.py @@ -0,0 +1,411 @@ +#!/usr/bin/env python3 +# +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Ironic Operator Charm. + +This charm provides Ironic API and noVNC Proxy services as part of an OpenStack +deployment. +""" + +import logging +import socket +from typing import ( + Dict, + List, + Mapping, +) + +import ops +import ops_sunbeam.charm as sunbeam_charm +import ops_sunbeam.container_handlers as sunbeam_chandlers +import ops_sunbeam.core as sunbeam_core +import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing +from ops.charm import ( + RelationEvent, +) + +logger = logging.getLogger(__name__) + +IRONIC_API_PORT = 6385 +IRONIC_API_CONTAINER = "ironic-api" +IRONIC_NOVNCPROXY_CONTAINER = "ironic-novncproxy" +IRONIC_NOVNCPROXY_INGRESS_NAME = "ironic-novncproxy" +IRONIC_API_PROVIDES = "ironic-api" + + +@sunbeam_tracing.trace_type +class IronicNoVNCProxyPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): + """Pebble handler for Ironic noVNC Proxy.""" + + def get_layer(self) -> dict: + """Ironic noVNC Proxy service layer. + + :returns: pebble layer configuration for the ironic-novncproxy service + :rtype: dict + """ + return { + "summary": "ironic novncproxy layer", + "description": "pebble configuration for ironic-novncproxy service", + "services": { + "ironic-novncproxy": { + "override": "replace", + "summary": "Ironic noVNC Proxy", + "command": "ironic-novncproxy", + "user": "ironic", + "group": "ironic", + } + }, + } + + def default_container_configs( + self, + ) -> List[sunbeam_core.ContainerConfigFile]: + """Container configuration files for handler.""" + _cconfigs = [ + sunbeam_core.ContainerConfigFile( + "/etc/ironic/ironic.conf", + "root", + "ironic", + 0o640, + ), + sunbeam_core.ContainerConfigFile( + "/etc/ironic/rootwrap.conf", + "root", + "ironic", + 0o640, + ), + ] + return _cconfigs + + +@sunbeam_tracing.trace_sunbeam_charm +class IronicOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): + """Charm the service.""" + + service_name = "ironic-api" + wsgi_admin_script = "/usr/bin/ironic-api-wsgi" + wsgi_public_script = "/usr/bin/ironic-api-wsgi" + + db_sync_cmds = [ + [ + "sudo", + "-u", + "ironic", + "ironic-dbsync", + ], + ] + + def __init__(self, framework): + super().__init__(framework) + self.traefik_route_public = None + self.traefik_route_internal = None + self.framework.observe( + self.on["peers"].relation_created, self._on_peer_relation_created + ) + self.framework.observe( + self.on["peers"].relation_departed, self._on_peer_relation_departed + ) + + @property + def service_conf(self) -> str: + """Service default configuration file.""" + return "/etc/ironic/ironic.conf" + + @property + def service_user(self) -> str: + """Service user file and directory ownership.""" + return "ironic" + + @property + def service_group(self) -> str: + """Service group file and directory ownership.""" + return "ironic" + + @property + def service_endpoints(self) -> List[Dict]: + """Service endpoints for the Ironic API services.""" + return [ + { + "service_name": "ironic", + "type": "baremetal", + "description": "OpenStack Ironic bare metal provisioner", + "internal_url": self.internal_url, + "public_url": self.public_url, + "admin_url": self.admin_url, + }, + ] + + @property + def default_public_ingress_port(self): + """Public ingress port for service.""" + return IRONIC_API_PORT + + @property + def ingress_healthcheck_path(self): + """Healthcheck path for ingress relation.""" + return "/healthcheck" + + @property + def healthcheck_http_url(self) -> str: + """Healthcheck HTTP URL for the service.""" + return ( + f"http://localhost:{self.default_public_ingress_port}/healthcheck" + ) + + @property + def databases(self) -> Mapping[str, str]: + """Provide database name for ironic services.""" + return {"database": "ironic"} + + def get_pebble_handlers( + self, + ) -> List[sunbeam_chandlers.ServicePebbleHandler]: + """Pebble handlers for the operator.""" + pebble_handlers = [ + sunbeam_chandlers.WSGIPebbleHandler( + self, + IRONIC_API_CONTAINER, + self.service_name, + self.container_configs, + self.template_dir, + self.configure_charm, + f"wsgi-{self.service_name}", + ), + IronicNoVNCProxyPebbleHandler( + self, + IRONIC_NOVNCPROXY_CONTAINER, + IRONIC_NOVNCPROXY_CONTAINER, + [], + self.template_dir, + self.configure_charm, + ), + ] + return pebble_handlers + + def post_config_setup(self): + """Configuration steps after services have been setup.""" + super().post_config_setup() + self.set_readiness_on_related_units() + + def handle_readiness_request_from_event( + self, event: RelationEvent + ) -> None: + """Set service readiness in relation data.""" + self.svc_ready_handler.interface.set_service_status( + event.relation, self.bootstrapped() + ) + + def set_readiness_on_related_units(self) -> None: + """Set service readiness on ironic-api related units.""" + logger.debug( + "Set service readiness on all connected placement relations" + ) + for relation in self.framework.model.relations[IRONIC_API_PROVIDES]: + self.svc_ready_handler.interface.set_service_status(relation, True) + + @property + def container_configs(self) -> List[sunbeam_core.ContainerConfigFile]: + """Container configuration files for the service.""" + _cconfigs = [ + sunbeam_core.ContainerConfigFile( + "/etc/ironic/api_audit_map.conf", + "root", + "ironic", + 0o640, + ), + sunbeam_core.ContainerConfigFile( + "/etc/ironic/ironic.conf", + "root", + "ironic", + 0o640, + ), + sunbeam_core.ContainerConfigFile( + "/etc/ironic/rootwrap.conf", + "root", + "ironic", + 0o640, + ), + ] + return _cconfigs + + @property + def db_sync_container_name(self) -> str: + """Name of Container to run db sync from.""" + return IRONIC_API_CONTAINER + + def get_relation_handlers( + self, handlers: List[sunbeam_rhandlers.RelationHandler] = None + ) -> List[sunbeam_rhandlers.RelationHandler]: + """Relation handlers for operator.""" + handlers = super().get_relation_handlers(handlers or []) + + self.svc_ready_handler = ( + sunbeam_rhandlers.ServiceReadinessProviderHandler( + self, + IRONIC_API_PROVIDES, + self.handle_readiness_request_from_event, + ) + ) + handlers.append(self.svc_ready_handler) + + self.traefik_route_public = sunbeam_rhandlers.TraefikRouteHandler( + self, + "traefik-route-public", + self.handle_traefik_public_ready, + "traefik-route-public" in self.mandatory_relations, + [IRONIC_NOVNCPROXY_INGRESS_NAME], + ) + handlers.append(self.traefik_route_public) + + self.traefik_route_internal = sunbeam_rhandlers.TraefikRouteHandler( + self, + "traefik-route-internal", + self.handle_traefik_internal_ready, + "traefik-route-internal" in self.mandatory_relations, + [IRONIC_NOVNCPROXY_INGRESS_NAME], + ) + handlers.append(self.traefik_route_internal) + + return handlers + + def _on_peer_relation_created( + self, event: ops.framework.EventBase + ) -> None: + logger.info("Setting peer unit data") + self.peers.set_unit_data({"host": socket.getfqdn()}) + + def _on_peer_relation_departed( + self, event: ops.framework.EventBase + ) -> None: + self.handle_traefik_ready(event) + + def handle_traefik_ready(self, event: ops.EventBase): + """Handle Traefik route ready callback.""" + self.handle_traefik_public_ready(event) + self.handle_traefik_internal_ready(event) + + def handle_traefik_public_ready(self, event: ops.EventBase): + """Handle Traefik public route ready callback.""" + if not self.unit.is_leader(): + logger.debug( + "Not a leader unit, skipping traefik public route config" + ) + return + + traefik = self.traefik_route_public + if traefik and traefik.interface.is_ready(): + logger.debug("Sending traefik config for public interface") + traefik.interface.submit_to_traefik(config=self.traefik_config) + + def handle_traefik_internal_ready(self, event: ops.EventBase): + """Handle Traefik internal route ready callback.""" + if not self.unit.is_leader(): + logger.debug( + "Not a leader unit, skipping traefik internal route config" + ) + return + + traefik = self.traefik_route_internal + if traefik and traefik.interface.is_ready(): + logger.debug("Sending traefik config for internal interface") + traefik.interface.submit_to_traefik(config=self.traefik_config) + + # traefik-route-internal is a mandatory relation. If this is the + # last relation added, run configure_charm to potentially kick it + # from the Waiting status. + self.configure_charm(event) + + @property + def traefik_config(self) -> dict: + """Config to publish to traefik.""" + model = self.model.name + router_cfg = {} + # Add router for ironic-novncproxy + # Rename router tls and add priority as workaround for + # bug https://github.com/canonical/traefik-k8s-operator/issues/335 + router_cfg.update( + { + f"juju-{model}-{IRONIC_NOVNCPROXY_INGRESS_NAME}-router": { + "rule": f"PathPrefix(`/{model}-{IRONIC_NOVNCPROXY_INGRESS_NAME}`)", + "middlewares": [ + "custom-stripprefix", + "custom-wsheaders-http", + ], + "service": f"juju-{model}-{IRONIC_NOVNCPROXY_INGRESS_NAME}-service", + "entryPoints": ["web"], + }, + f"juju-{model}-{IRONIC_NOVNCPROXY_INGRESS_NAME}-router-https": { + "rule": f"PathPrefix(`/{model}-{IRONIC_NOVNCPROXY_INGRESS_NAME}`)", + "middlewares": [ + "custom-stripprefix", + "custom-wsheaders-https", + ], + "service": f"juju-{model}-{IRONIC_NOVNCPROXY_INGRESS_NAME}-service", + "entryPoints": ["websecure"], + "tls": {}, + "priority": 100, + }, + } + ) + + # Add middlewares to ironic-novncproxy + middleware_cfg = { + "custom-stripprefix": { + "stripPrefix": { + "prefixes": [f"/{model}-{IRONIC_NOVNCPROXY_INGRESS_NAME}"], + "forceSlash": False, + } + }, + "custom-wsheaders-http": { + "headers": { + "customRequestHeaders": {"X-Forwarded-Proto": "http"} + } + }, + "custom-wsheaders-https": { + "headers": { + "customRequestHeaders": {"X-Forwarded-Proto": "https"} + } + }, + } + + # Get host key value from all units + hosts = self.peers.get_all_unit_values( + key="host", include_local_unit=True + ) + novnc_lb_servers = [ + {"url": f"http://{host}:{IRONIC_NOVNCPROXY_INGRESS_NAME}"} + for host in hosts + ] + # Add services for heat-api and heat-api-cfn + service_cfg = { + f"juju-{model}-{IRONIC_NOVNCPROXY_INGRESS_NAME}-service": { + "loadBalancer": {"servers": novnc_lb_servers}, + }, + } + + config = { + "http": { + "routers": router_cfg, + "middlewares": middleware_cfg, + "services": service_cfg, + }, + } + return config + + +if __name__ == "__main__": # pragma: nocover + ops.main(IronicOperatorCharm) diff --git a/charms/ironic-k8s/src/templates/api_audit_map.conf b/charms/ironic-k8s/src/templates/api_audit_map.conf new file mode 100644 index 00000000..b2569def --- /dev/null +++ b/charms/ironic-k8s/src/templates/api_audit_map.conf @@ -0,0 +1,29 @@ +[DEFAULT] +# default target endpoint type +# should match the endpoint type defined in service catalog +target_endpoint_type = None + +# possible end path of API requests +# path of api requests for CADF target typeURI +# Just need to include top resource path to identify class +# of resources. Ex: Log audit event for API requests +# path containing "nodes" keyword and node uuid. +[path_keywords] +nodes = node +drivers = driver +chassis = chassis +ports = port +states = state +power = None +provision = None +maintenance = None +validate = None +boot_device = None +supported = None +console = None +vendor_passthru = vendor_passthru + + +# map endpoint type defined in service catalog to CADF typeURI +[service_endpoints] +baremetal = service/compute/baremetal diff --git a/charms/ironic-k8s/src/templates/ironic.conf.j2 b/charms/ironic-k8s/src/templates/ironic.conf.j2 new file mode 100644 index 00000000..c990335d --- /dev/null +++ b/charms/ironic-k8s/src/templates/ironic.conf.j2 @@ -0,0 +1,31 @@ +############################################## +# [ WARNING ] +# ironic configuration file maintained by Juju +# local changes may be overwritten. +############################################## + +[DEFAULT] +debug = {{ options.debug }} +transport_url = {{ amqp.transport_url }} +auth_strategy = keystone + +{% include "parts/section-database" %} + +{% include "parts/section-identity" %} + +{% include "parts/section-service-user" %} + +{% include "parts/section-oslo-messaging-rabbit" %} + +[oslo_concurrency] +lock_path = /var/lib/ironic/tmp + +[audit] +enabled = true +audit_map_file = /etc/ironic/api_audit_map.conf + +[healthcheck] +enabled = true + +[vnc] +enabled = true diff --git a/charms/ironic-k8s/src/templates/rootwrap.conf b/charms/ironic-k8s/src/templates/rootwrap.conf new file mode 100644 index 00000000..c813c6cd --- /dev/null +++ b/charms/ironic-k8s/src/templates/rootwrap.conf @@ -0,0 +1,27 @@ +# Configuration for ironic-rootwrap +# This file should be owned by (and only writable by) the root user + +[DEFAULT] +# List of directories to load filter definitions from (separated by ','). +# These directories MUST all be only writable by root ! +filters_path=/etc/ironic/rootwrap.d + +# List of directories to search executables in, in case filters do not +# explicitly specify a full path (separated by ',') +# If not specified, defaults to system PATH environment variable. +# These directories MUST all be only writable by root ! +exec_dirs=/sbin,/usr/sbin,/bin,/usr/bin + +# Enable logging to syslog +# Default value is False +use_syslog=False + +# Which syslog facility to use. +# Valid values include auth, authpriv, syslog, user0, user1... +# Default value is 'syslog' +syslog_log_facility=syslog + +# Which messages to log. +# INFO means log all usage +# ERROR means only log unsuccessful attempts +syslog_log_level=ERROR diff --git a/charms/ironic-k8s/src/templates/wsgi-ironic-api.conf.j2 b/charms/ironic-k8s/src/templates/wsgi-ironic-api.conf.j2 new file mode 100644 index 00000000..612ffafb --- /dev/null +++ b/charms/ironic-k8s/src/templates/wsgi-ironic-api.conf.j2 @@ -0,0 +1,28 @@ +Listen 6385 +LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\" %D(us)" ironic_combined + + + WSGIDaemonProcess ironic processes=2 threads=1 user=ironic group=ironic display-name=%{GROUP} + WSGIProcessGroup ironic + {% if ingress_internal and 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 + = 2.4> + ErrorLogFormat "%{cu}t %M" + + ErrorLog {{ wsgi_config.error_log }} + CustomLog {{ wsgi_config.custom_log }} combined + + + = 2.4> + Require all granted + + + Order allow,deny + Allow from all + + + diff --git a/charms/ironic-k8s/tests/unit/__init__.py b/charms/ironic-k8s/tests/unit/__init__.py new file mode 100644 index 00000000..5ae022e4 --- /dev/null +++ b/charms/ironic-k8s/tests/unit/__init__.py @@ -0,0 +1,16 @@ +# +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for Ironic K8S Operator.""" diff --git a/charms/ironic-k8s/tests/unit/test_charm.py b/charms/ironic-k8s/tests/unit/test_charm.py new file mode 100644 index 00000000..acb1ea92 --- /dev/null +++ b/charms/ironic-k8s/tests/unit/test_charm.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 + +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for the Ironic K8s charm.""" + +import charm +import ops_sunbeam.test_utils as test_utils +from ops.testing import ( + Harness, +) + + +class _IronicOperatorCharm(charm.IronicOperatorCharm): + """Test implementation of Ironic operator.""" + + def __init__(self, framework): + self.seen_events = [] + self.render_calls = [] + super().__init__(framework) + + def _log_event(self, event): + self.seen_events.append(type(event).__name__) + + def renderer(self, containers, container_configs, template_dir, adapters): + """Intercept and record all calls to render config files.""" + self.render_calls.append( + (containers, container_configs, template_dir, adapters) + ) + + def configure_charm(self, event): + """Intercept and record full charm configuration events.""" + super().configure_charm(event) + self._log_event(event) + + @property + def public_ingress_address(self): + return "ironic.juju" + + +class TestIronicOperatorCharm(test_utils.CharmTestCase): + """Unit tests for Ironic Operator.""" + + PATCHES = [] + + def setUp(self): + """Setup test fixtures for test.""" + super().setUp(charm, self.PATCHES) + self.harness = test_utils.get_harness( + _IronicOperatorCharm, container_calls=self.container_calls + ) + + # clean up events that were dynamically defined, + # otherwise we get issues because they'll be redefined, + # which is not allowed. + from charms.data_platform_libs.v0.data_interfaces import ( + DatabaseRequiresEvents, + ) + + for attr in ( + "database_database_created", + "database_endpoints_changed", + "database_read_only_endpoints_changed", + ): + try: + delattr(DatabaseRequiresEvents, attr) + except AttributeError: + pass + + self.addCleanup(self.harness.cleanup) + self.harness.begin() + + def add_ironic_api_relation(self): + """Add ironic-api relation.""" + return self.harness.add_relation(charm.IRONIC_API_PROVIDES, "consumer") + + def add_db_relation(self, harness: Harness, name: str) -> str: + """Add db relation.""" + rel_id = harness.add_relation(name, "mysql") + harness.add_relation_unit(rel_id, "mysql/0") + harness.update_relation_data( + rel_id, "mysql/0", {"ingress-address": "10.0.0.3"} + ) + return rel_id + + def add_complete_ingress_relation(self) -> None: + """Add complete ingress and traefik-route relations.""" + test_utils.add_complete_ingress_relation(self.harness) + self.harness.add_relation( + "traefik-route-public", + "traefik", + app_data={"external_host": "dummy-ip", "scheme": "http"}, + ) + self.harness.add_relation( + "traefik-route-internal", + "ironic", + app_data={"external_host": "dummy-ip", "scheme": "http"}, + ) + + def test_pebble_ready_handler(self): + """Test pebble ready event handling.""" + self.assertEqual(self.harness.charm.seen_events, []) + test_utils.set_all_pebbles_ready(self.harness) + self.assertEqual(len(self.harness.charm.seen_events), 2) + + def test_all_relations(self): + """Test all integrations for operator.""" + self.harness.set_leader() + test_utils.set_all_pebbles_ready(self.harness) + self.add_ironic_api_relation() + + ironic_api_rel = self.harness.model.get_relation("ironic-api") + rel_data = ironic_api_rel.data[self.harness.model.app] + self.assertEqual({}, rel_data) + + # this adds all the default/common relations + test_utils.add_all_relations(self.harness) + self.add_complete_ingress_relation() + + setup_cmds = [ + ["a2ensite", "wsgi-ironic-api"], + ] + for cmd in setup_cmds: + self.assertIn(cmd, self.container_calls.execute["ironic-api"]) + + config_files = [ + "/etc/apache2/sites-available/wsgi-ironic-api.conf", + "/etc/ironic/api_audit_map.conf", + "/etc/ironic/ironic.conf", + "/etc/ironic/rootwrap.conf", + ] + for f in config_files: + self.check_file("ironic-api", f) + + config_files = [ + "/etc/ironic/ironic.conf", + "/etc/ironic/rootwrap.conf", + ] + for f in config_files: + self.check_file("ironic-novncproxy", f) + + self.assertEqual({"ready": "true"}, rel_data) diff --git a/charms/ironic-k8s/uv.lock b/charms/ironic-k8s/uv.lock new file mode 100644 index 00000000..d4b24a1f --- /dev/null +++ b/charms/ironic-k8s/uv.lock @@ -0,0 +1,323 @@ +version = 1 +revision = 3 +requires-python = "==3.12.*" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "deprecated" +version = "1.2.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744, upload-time = "2025-01-27T10:46:25.7Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998, upload-time = "2025-01-27T10:46:09.186Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "6.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/eb/58c2ab27ee628ad801f56d4017fe62afab0293116f6d0b08f1d5bd46e06f/importlib_metadata-6.11.0.tar.gz", hash = "sha256:1231cf92d825c9e03cfc4da076a16de6422c863558229ea0b22b675657463443", size = 54593, upload-time = "2023-12-03T17:33:10.693Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/9b/ecce94952ab5ea74c31dcf9ccf78ccd484eebebef06019bf8cb579ab4519/importlib_metadata-6.11.0-py3-none-any.whl", hash = "sha256:f0afba6205ad8f8947c7d338b5342d5db2afbfd82f9cbef7879a9539cc12eb9b", size = 23427, upload-time = "2023-12-03T17:33:08.965Z" }, +] + +[[package]] +name = "interface-tls-certificates" +version = "0.0.1.dev1" +source = { git = "https://opendev.org/openstack/charm-ops-interface-tls-certificates#f720af03a1acb640ab1fed86fd30ff9aafda1ea4" } +dependencies = [ + { name = "ops" }, +] + +[[package]] +name = "ironic-k8s" +version = "2025.1" +source = { virtual = "." } +dependencies = [ + { name = "interface-tls-certificates" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "opentelemetry-api" }, + { name = "ops" }, + { name = "pydantic" }, + { name = "tenacity" }, +] + +[package.metadata] +requires-dist = [ + { name = "interface-tls-certificates", git = "https://opendev.org/openstack/charm-ops-interface-tls-certificates" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "opentelemetry-api", specifier = "~=1.21.0" }, + { name = "ops" }, + { name = "pydantic" }, + { name = "tenacity" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778, upload-time = "2024-07-08T18:40:05.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462, upload-time = "2024-07-08T18:40:00.165Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "importlib-metadata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/aa/1a10f310275fdd05a1062d4a8a641a5f041db2377956a80ff3c4dc325a6c/opentelemetry_api-1.21.0.tar.gz", hash = "sha256:d6185fd5043e000075d921822fd2d26b953eba8ca21b1e2fa360dd46a7686316", size = 56674, upload-time = "2023-11-07T23:16:23.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/3a/945e6c21f405ac4ea526f91ee09cc1568c04e0c95d3392903e6984c8f0e0/opentelemetry_api-1.21.0-py3-none-any.whl", hash = "sha256:4bb86b28627b7e41098f0e93280fe4892a1abed1b79a19aec6f928f39b17dffb", size = 57947, upload-time = "2023-11-07T23:15:46.656Z" }, +] + +[[package]] +name = "ops" +version = "2.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "opentelemetry-api" }, + { name = "pyyaml" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/c5/f0098a9b1b72680b3682043227a628a08a7b5b9592fc98ea6efa0d638017/ops-2.21.1.tar.gz", hash = "sha256:4a8190420813ba37e7a0399d656008f99c79015d7f72e138bad7cb1ac403d0b0", size = 496427, upload-time = "2025-05-01T03:03:23.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/c7/b70271ee12418144d5c596f37745c21da105470d365d834a9fce071f7bc2/ops-2.21.1-py3-none-any.whl", hash = "sha256:6745084c8e73bc485c254f95664bd85ddcf786c91b90782f2830c8333a1440b2", size = 182682, upload-time = "2025-05-01T03:03:20.946Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/ab/5250d56ad03884ab5efd07f734203943c8a8ab40d551e208af81d0257bf2/pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d", size = 786540, upload-time = "2025-04-29T20:38:55.02Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/12/46b65f3534d099349e38ef6ec98b1a5a81f42536d17e0ba382c28c67ba67/pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb", size = 443900, upload-time = "2025-04-29T20:38:52.724Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/b3/52b213298a0ba7097c7ea96bee95e1947aa84cc816d48cebb539770cdf41/rpds_py-0.24.0.tar.gz", hash = "sha256:772cc1b2cd963e7e17e6cc55fe0371fb9c704d63e44cacec7b9b7f523b78919e", size = 26863, upload-time = "2025-03-26T14:56:01.518Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/e0/1c55f4a3be5f1ca1a4fd1f3ff1504a1478c1ed48d84de24574c4fa87e921/rpds_py-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d8551e733626afec514b5d15befabea0dd70a343a9f23322860c4f16a9430205", size = 366945, upload-time = "2025-03-26T14:53:28.149Z" }, + { url = "https://files.pythonhosted.org/packages/39/1b/a3501574fbf29118164314dbc800d568b8c1c7b3258b505360e8abb3902c/rpds_py-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e374c0ce0ca82e5b67cd61fb964077d40ec177dd2c4eda67dba130de09085c7", size = 351935, upload-time = "2025-03-26T14:53:29.684Z" }, + { url = "https://files.pythonhosted.org/packages/dc/47/77d3d71c55f6a374edde29f1aca0b2e547325ed00a9da820cabbc9497d2b/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d69d003296df4840bd445a5d15fa5b6ff6ac40496f956a221c4d1f6f7b4bc4d9", size = 390817, upload-time = "2025-03-26T14:53:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ec/1e336ee27484379e19c7f9cc170f4217c608aee406d3ae3a2e45336bff36/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8212ff58ac6dfde49946bea57474a386cca3f7706fc72c25b772b9ca4af6b79e", size = 401983, upload-time = "2025-03-26T14:53:33.163Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/39b65cbc272c635eaea6d393c2ad1ccc81c39eca2db6723a0ca4b2108fce/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:528927e63a70b4d5f3f5ccc1fa988a35456eb5d15f804d276709c33fc2f19bda", size = 451719, upload-time = "2025-03-26T14:53:34.721Z" }, + { url = "https://files.pythonhosted.org/packages/32/05/05c2b27dd9c30432f31738afed0300659cb9415db0ff7429b05dfb09bbde/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a824d2c7a703ba6daaca848f9c3d5cb93af0505be505de70e7e66829affd676e", size = 442546, upload-time = "2025-03-26T14:53:36.26Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e0/19383c8b5d509bd741532a47821c3e96acf4543d0832beba41b4434bcc49/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d51febb7a114293ffd56c6cf4736cb31cd68c0fddd6aa303ed09ea5a48e029", size = 393695, upload-time = "2025-03-26T14:53:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/9d/15/39f14e96d94981d0275715ae8ea564772237f3fa89bc3c21e24de934f2c7/rpds_py-0.24.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3fab5f4a2c64a8fb64fc13b3d139848817a64d467dd6ed60dcdd6b479e7febc9", size = 427218, upload-time = "2025-03-26T14:53:39.326Z" }, + { url = "https://files.pythonhosted.org/packages/22/b9/12da7124905a680f690da7a9de6f11de770b5e359f5649972f7181c8bf51/rpds_py-0.24.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9be4f99bee42ac107870c61dfdb294d912bf81c3c6d45538aad7aecab468b6b7", size = 568062, upload-time = "2025-03-26T14:53:40.885Z" }, + { url = "https://files.pythonhosted.org/packages/88/17/75229017a2143d915f6f803721a6d721eca24f2659c5718a538afa276b4f/rpds_py-0.24.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:564c96b6076a98215af52f55efa90d8419cc2ef45d99e314fddefe816bc24f91", size = 596262, upload-time = "2025-03-26T14:53:42.544Z" }, + { url = "https://files.pythonhosted.org/packages/aa/64/8e8a1d8bd1b6b638d6acb6d41ab2cec7f2067a5b8b4c9175703875159a7c/rpds_py-0.24.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:75a810b7664c17f24bf2ffd7f92416c00ec84b49bb68e6a0d93e542406336b56", size = 564306, upload-time = "2025-03-26T14:53:44.2Z" }, + { url = "https://files.pythonhosted.org/packages/68/1c/a7eac8d8ed8cb234a9b1064647824c387753343c3fab6ed7c83481ed0be7/rpds_py-0.24.0-cp312-cp312-win32.whl", hash = "sha256:f6016bd950be4dcd047b7475fdf55fb1e1f59fc7403f387be0e8123e4a576d30", size = 224281, upload-time = "2025-03-26T14:53:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/bb/46/b8b5424d1d21f2f2f3f2d468660085318d4f74a8df8289e3dd6ad224d488/rpds_py-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:998c01b8e71cf051c28f5d6f1187abbdf5cf45fc0efce5da6c06447cba997034", size = 239719, upload-time = "2025-03-26T14:53:47.187Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222, upload-time = "2025-02-25T17:27:59.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload-time = "2025-02-25T17:27:57.754Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload-time = "2024-04-23T22:16:16.976Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload-time = "2024-04-23T22:16:14.422Z" }, +] + +[[package]] +name = "wrapt" +version = "1.17.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531, upload-time = "2025-01-14T10:35:45.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799, upload-time = "2025-01-14T10:33:57.4Z" }, + { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821, upload-time = "2025-01-14T10:33:59.334Z" }, + { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919, upload-time = "2025-01-14T10:34:04.093Z" }, + { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721, upload-time = "2025-01-14T10:34:07.163Z" }, + { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899, upload-time = "2025-01-14T10:34:09.82Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222, upload-time = "2025-01-14T10:34:11.258Z" }, + { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707, upload-time = "2025-01-14T10:34:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685, upload-time = "2025-01-14T10:34:15.043Z" }, + { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567, upload-time = "2025-01-14T10:34:16.563Z" }, + { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672, upload-time = "2025-01-14T10:34:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865, upload-time = "2025-01-14T10:34:19.577Z" }, + { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594, upload-time = "2025-01-14T10:35:44.018Z" }, +] + +[[package]] +name = "zipp" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545, upload-time = "2024-11-10T15:05:20.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630, upload-time = "2024-11-10T15:05:19.275Z" }, +] diff --git a/tests/all-k8s/smoke.yaml.j2 b/tests/all-k8s/smoke.yaml.j2 index 66452f92..e14e5831 100644 --- a/tests/all-k8s/smoke.yaml.j2 +++ b/tests/all-k8s/smoke.yaml.j2 @@ -225,6 +225,20 @@ applications: resources: heat-api-image: ghcr.io/canonical/heat-consolidated:2025.1 heat-engine-image: ghcr.io/canonical/heat-consolidated:2025.1 + # Bare metal feature + ironic: + {% if ironic_k8s is defined and ironic_k8s is sameas true -%} + charm: ../../../ironic-k8s.charm + {% else -%} + charm: ch:ironic-k8s + channel: 2025.1/edge + {% endif -%} + base: ubuntu@24.04 + scale: 1 + trust: true + resources: + ironic-api-image: ghcr.io/canonical/ironic-consolidated:2025.1 + ironic-novncproxy-image: ghcr.io/canonical/ironic-consolidated:2025.1 # Load Balancer feature octavia: {% if octavia_k8s is defined and octavia_k8s is sameas true -%} @@ -493,6 +507,19 @@ relations: - - keystone:send-ca-cert - heat:receive-ca-cert +- - mysql:database + - ironic:database +- - keystone:identity-service + - ironic:identity-service +- - traefik:ingress + - ironic:ingress-internal +- - traefik:traefik-route + - ironic:traefik-route-internal +- - rabbitmq:amqp + - ironic:amqp +- - keystone:send-ca-cert + - ironic:receive-ca-cert + - - mysql:database - octavia:database - - keystone:identity-service diff --git a/tests/all-k8s/tests.yaml b/tests/all-k8s/tests.yaml index b722dbbd..eb50099f 100644 --- a/tests/all-k8s/tests.yaml +++ b/tests/all-k8s/tests.yaml @@ -94,6 +94,9 @@ target_deploy_status: heat: workload-status: active workload-status-message-regex: '^.*$' + ironic: + workload-status: active + workload-status-message-regex: '^.*$' octavia: workload-status: active workload-status-message-regex: '^$' diff --git a/zuul.d/jobs.yaml b/zuul.d/jobs.yaml index a417b546..85d7d283 100644 --- a/zuul.d/jobs.yaml +++ b/zuul.d/jobs.yaml @@ -178,6 +178,18 @@ - rebuild vars: charm: heat-k8s +- job: + name: charm-build-ironic-k8s + description: Build sunbeam ironic-k8s charm + run: playbooks/charm/build.yaml + timeout: 3600 + match-on-config-updates: false + files: + - ops-sunbeam/ops_sunbeam/ + - charms/ironic-k8s/ + - rebuild + vars: + charm: ironic-k8s - job: name: charm-build-octavia-k8s description: Build sunbeam octavia-k8s charm @@ -864,6 +876,19 @@ - charmhub_token timeout: 3600 +- job: + name: publish-charm-ironic-k8s + description: | + Publish ironic-k8s built in gate pipeline. + run: playbooks/charm/publish.yaml + files: + - ops-sunbeam/ops_sunbeam/ + - charms/ironic-k8s/ + - rebuild + secrets: + - charmhub_token + timeout: 3600 + - job: name: publish-charm-horizon-k8s description: | @@ -1176,6 +1201,8 @@ soft: true - name: charm-build-heat-k8s soft: true + - name: charm-build-ironic-k8s + soft: true - name: charm-build-octavia-k8s soft: true - name: charm-build-barbican-k8s @@ -1215,6 +1242,7 @@ - charms/aodh-k8s/ - charms/watcher-k8s/ - charms/heat-k8s/ + - charms/ironic-k8s/ - charms/octavia-k8s/ - charms/barbican-k8s/ - charms/magnum-k8s/ @@ -1246,6 +1274,7 @@ - charm-build-aodh-k8s - charm-build-watcher-k8s - charm-build-heat-k8s + - charm-build-ironic-k8s - charm-build-octavia-k8s - charm-build-barbican-k8s - charm-build-magnum-k8s diff --git a/zuul.d/project-templates.yaml b/zuul.d/project-templates.yaml index 4a3f2615..b82416f5 100644 --- a/zuul.d/project-templates.yaml +++ b/zuul.d/project-templates.yaml @@ -66,6 +66,8 @@ nodeset: ubuntu-jammy - charm-build-heat-k8s: nodeset: ubuntu-jammy + - charm-build-ironic-k8s: + nodeset: ubuntu-jammy - charm-build-octavia-k8s: nodeset: ubuntu-jammy - charm-build-aodh-k8s: @@ -137,6 +139,8 @@ nodeset: ubuntu-jammy - charm-build-heat-k8s: nodeset: ubuntu-jammy + - charm-build-ironic-k8s: + nodeset: ubuntu-jammy - charm-build-octavia-k8s: nodeset: ubuntu-jammy - charm-build-aodh-k8s: @@ -212,6 +216,8 @@ nodeset: ubuntu-jammy - publish-charm-heat-k8s: nodeset: ubuntu-jammy + - publish-charm-ironic-k8s: + nodeset: ubuntu-jammy - publish-charm-octavia-k8s: nodeset: ubuntu-jammy - publish-charm-aodh-k8s: diff --git a/zuul.d/zuul.yaml b/zuul.d/zuul.yaml index e54d7dbe..7b545d8a 100644 --- a/zuul.d/zuul.yaml +++ b/zuul.d/zuul.yaml @@ -35,6 +35,7 @@ cinder-ceph-k8s: 2025.1/edge horizon-k8s: 2025.1/edge heat-k8s: 2025.1/edge + ironic-k8s: 2025.1/edge octavia-k8s: 2025.1/edge aodh-k8s: 2025.1/edge ceilometer-k8s: 2025.1/edge