First cut

This commit is contained in:
Liam Young 2023-08-09 14:44:02 +00:00
commit 955c89fb54
39 changed files with 1871 additions and 0 deletions

11
charms/ceilometer-k8s/.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
venv/
build/
*.charm
.tox/
.coverage
__pycache__/
*.py[cod]
.idea
.vscode/
*.swp
.stestr/

View File

@ -0,0 +1,5 @@
[gerrit]
host=review.opendev.org
port=29418
project=openstack/charm-ceilometer-k8s.git
defaultbranch=main

View File

@ -0,0 +1,3 @@
[DEFAULT]
test_path=./tests/unit
top_dir=./tests

View File

@ -0,0 +1,33 @@
# Contributing
To make contributions to this charm, you'll need a working [development setup](https://juju.is/docs/sdk/dev-setup).
You can use the environments created by `tox` for development:
```shell
tox --notest -e unit
source .tox/unit/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 -e fmt # update your code according to linting rules
tox -e lint # code style
tox -e unit # unit tests
tox -e integration # integration tests
tox # runs 'lint' and 'unit' environments
```
## Build 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>

View File

@ -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 liam
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -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.
-->
# ceilometer-k8s
Charmhub package name: operator-template
More information: https://charmhub.io/ceilometer-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.

View File

@ -0,0 +1,2 @@
# NOTE: no actions yet!
{ }

View File

@ -0,0 +1,30 @@
type: "charm"
bases:
- build-on:
- name: "ubuntu"
channel: "22.04"
run-on:
- name: "ubuntu"
channel: "22.04"
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
- cargo
- pkg-config
charm-binary-python-packages:
- cryptography
- jsonschema
- jinja2
- git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam

View File

@ -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

View File

@ -0,0 +1,7 @@
#!/bin/bash
echo "INFO: Fetching libs from charmhub."
# charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
# charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
# charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
# charmcraft fetch-lib charms.traefik_k8s.v1.ingress

View File

@ -0,0 +1,458 @@
"""IdentityCredentialsProvides and Requires module.
This library contains the Requires and Provides classes for handling
the identity_credentials interface.
Import `IdentityCredentialsRequires` in your charm, with the charm object and the
relation name:
- self
- "identity_credentials"
Also provide additional parameters to the charm object:
- service
- internal_url
- public_url
- admin_url
- region
- username
- vhost
Two events are also available to respond to:
- connected
- ready
- goneaway
A basic example showing the usage of this relation follows:
```
from charms.keystone_k8s.v0.identity_credentials import IdentityCredentialsRequires
class IdentityCredentialsClientCharm(CharmBase):
def __init__(self, *args):
super().__init__(*args)
# IdentityCredentials Requires
self.identity_credentials = IdentityCredentialsRequires(
self, "identity_credentials",
service = "my-service"
internal_url = "http://internal-url"
public_url = "http://public-url"
admin_url = "http://admin-url"
region = "region"
)
self.framework.observe(
self.identity_credentials.on.connected, self._on_identity_credentials_connected)
self.framework.observe(
self.identity_credentials.on.ready, self._on_identity_credentials_ready)
self.framework.observe(
self.identity_credentials.on.goneaway, self._on_identity_credentials_goneaway)
def _on_identity_credentials_connected(self, event):
'''React to the IdentityCredentials connected event.
This event happens when IdentityCredentials relation is added to the
model before credentials etc have been provided.
'''
# Do something before the relation is complete
pass
def _on_identity_credentials_ready(self, event):
'''React to the IdentityCredentials ready event.
The IdentityCredentials interface will use the provided config for the
request to the identity server.
'''
# IdentityCredentials Relation is ready. Do something with the completed relation.
pass
def _on_identity_credentials_goneaway(self, event):
'''React to the IdentityCredentials goneaway event.
This event happens when an IdentityCredentials relation is removed.
'''
# IdentityCredentials Relation has goneaway. shutdown services or suchlike
pass
```
"""
import logging
from ops.framework import (
StoredState,
EventBase,
ObjectEvents,
EventSource,
Object,
)
from ops.model import (
Relation,
SecretNotFoundError,
)
# The unique Charmhub library identifier, never change it
LIBID = "b5fa18d4427c4ab9a269c3a2fbed545c"
# Increment this major API version when introducing breaking changes
LIBAPI = 0
# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 3
logger = logging.getLogger(__name__)
class IdentityCredentialsConnectedEvent(EventBase):
"""IdentityCredentials connected Event."""
pass
class IdentityCredentialsReadyEvent(EventBase):
"""IdentityCredentials ready for use Event."""
pass
class IdentityCredentialsGoneAwayEvent(EventBase):
"""IdentityCredentials relation has gone-away Event"""
pass
class IdentityCredentialsServerEvents(ObjectEvents):
"""Events class for `on`"""
connected = EventSource(IdentityCredentialsConnectedEvent)
ready = EventSource(IdentityCredentialsReadyEvent)
goneaway = EventSource(IdentityCredentialsGoneAwayEvent)
class IdentityCredentialsRequires(Object):
"""
IdentityCredentialsRequires class
"""
on = IdentityCredentialsServerEvents()
_stored = StoredState()
def __init__(self, charm, relation_name: str):
super().__init__(charm, relation_name)
self.charm = charm
self.relation_name = relation_name
self.framework.observe(
self.charm.on[relation_name].relation_joined,
self._on_identity_credentials_relation_joined,
)
self.framework.observe(
self.charm.on[relation_name].relation_changed,
self._on_identity_credentials_relation_changed,
)
self.framework.observe(
self.charm.on[relation_name].relation_departed,
self._on_identity_credentials_relation_changed,
)
self.framework.observe(
self.charm.on[relation_name].relation_broken,
self._on_identity_credentials_relation_broken,
)
def _on_identity_credentials_relation_joined(self, event):
"""IdentityCredentials relation joined."""
logging.debug("IdentityCredentials on_joined")
self.on.connected.emit()
self.request_credentials()
def _on_identity_credentials_relation_changed(self, event):
"""IdentityCredentials relation changed."""
logging.debug("IdentityCredentials on_changed")
try:
self.on.ready.emit()
except (AttributeError, KeyError):
logger.exception('Error when emitting event')
def _on_identity_credentials_relation_broken(self, event):
"""IdentityCredentials relation broken."""
logging.debug("IdentityCredentials on_broken")
self.on.goneaway.emit()
@property
def _identity_credentials_rel(self) -> Relation:
"""The IdentityCredentials relation."""
return self.framework.model.get_relation(self.relation_name)
def get_remote_app_data(self, key: str) -> str:
"""Return the value for the given key from remote app data."""
data = self._identity_credentials_rel.data[self._identity_credentials_rel.app]
return data.get(key)
@property
def api_version(self) -> str:
"""Return the api_version."""
return self.get_remote_app_data('api-version')
@property
def auth_host(self) -> str:
"""Return the auth_host."""
return self.get_remote_app_data('auth-host')
@property
def auth_port(self) -> str:
"""Return the auth_port."""
return self.get_remote_app_data('auth-port')
@property
def auth_protocol(self) -> str:
"""Return the auth_protocol."""
return self.get_remote_app_data('auth-protocol')
@property
def internal_host(self) -> str:
"""Return the internal_host."""
return self.get_remote_app_data('internal-host')
@property
def internal_port(self) -> str:
"""Return the internal_port."""
return self.get_remote_app_data('internal-port')
@property
def internal_protocol(self) -> str:
"""Return the internal_protocol."""
return self.get_remote_app_data('internal-protocol')
@property
def credentials(self) -> str:
return self.get_remote_app_data('credentials')
@property
def username(self) -> str:
credentials_id = self.get_remote_app_data('credentials')
if not credentials_id:
return None
try:
credentials = self.charm.model.get_secret(id=credentials_id)
return credentials.get_content().get("username")
except SecretNotFoundError:
logger.warning(f"Secret {credentials_id} not found")
return None
@property
def password(self) -> str:
credentials_id = self.get_remote_app_data('credentials')
if not credentials_id:
return None
try:
credentials = self.charm.model.get_secret(id=credentials_id)
return credentials.get_content().get("password")
except SecretNotFoundError:
logger.warning(f"Secret {credentials_id} not found")
return None
@property
def project_name(self) -> str:
"""Return the project name."""
return self.get_remote_app_data('project-name')
@property
def project_id(self) -> str:
"""Return the project id."""
return self.get_remote_app_data('project-id')
@property
def user_domain_name(self) -> str:
"""Return the name of the user domain."""
return self.get_remote_app_data('user-domain-name')
@property
def user_domain_id(self) -> str:
"""Return the id of the user domain."""
return self.get_remote_app_data('user-domain-id')
@property
def project_domain_name(self) -> str:
"""Return the name of the project domain."""
return self.get_remote_app_data('project-domain-name')
@property
def project_domain_id(self) -> str:
"""Return the id of the project domain."""
return self.get_remote_app_data('project-domain-id')
@property
def region(self) -> str:
"""Return the region for the auth urls."""
return self.get_remote_app_data('region')
@property
def internal_endpoint(self) -> str:
"""Return the region for the internal auth url."""
return self.get_remote_app_data('internal-endpoint')
@property
def public_endpoint(self) -> str:
"""Return the region for the public auth url."""
return self.get_remote_app_data('public-endpoint')
@property
def admin_role(self) -> str:
"""Return the admin_role."""
return self.get_remote_app_data('admin-role')
def request_credentials(self) -> None:
"""Request credentials from the IdentityCredentials server."""
if self.model.unit.is_leader():
logging.debug(f'Requesting credentials for {self.charm.app.name}')
app_data = self._identity_credentials_rel.data[self.charm.app]
app_data['username'] = self.charm.app.name
class HasIdentityCredentialsClientsEvent(EventBase):
"""Has IdentityCredentialsClients Event."""
pass
class ReadyIdentityCredentialsClientsEvent(EventBase):
"""IdentityCredentialsClients Ready Event."""
def __init__(self, handle, relation_id, relation_name, username):
super().__init__(handle)
self.relation_id = relation_id
self.relation_name = relation_name
self.username = username
def snapshot(self):
return {
"relation_id": self.relation_id,
"relation_name": self.relation_name,
"username": self.username,
}
def restore(self, snapshot):
super().restore(snapshot)
self.relation_id = snapshot["relation_id"]
self.relation_name = snapshot["relation_name"]
self.username = snapshot["username"]
class IdentityCredentialsClientsGoneAwayEvent(EventBase):
"""Has IdentityCredentialsClientsGoneAwayEvent Event."""
pass
class IdentityCredentialsClientEvents(ObjectEvents):
"""Events class for `on`"""
has_identity_credentials_clients = EventSource(
HasIdentityCredentialsClientsEvent
)
ready_identity_credentials_clients = EventSource(
ReadyIdentityCredentialsClientsEvent
)
identity_credentials_clients_gone = EventSource(
IdentityCredentialsClientsGoneAwayEvent
)
class IdentityCredentialsProvides(Object):
"""
IdentityCredentialsProvides class
"""
on = IdentityCredentialsClientEvents()
_stored = StoredState()
def __init__(self, charm, relation_name):
super().__init__(charm, relation_name)
self.charm = charm
self.relation_name = relation_name
self.framework.observe(
self.charm.on[relation_name].relation_joined,
self._on_identity_credentials_relation_joined,
)
self.framework.observe(
self.charm.on[relation_name].relation_changed,
self._on_identity_credentials_relation_changed,
)
self.framework.observe(
self.charm.on[relation_name].relation_broken,
self._on_identity_credentials_relation_broken,
)
def _on_identity_credentials_relation_joined(self, event):
"""Handle IdentityCredentials joined."""
logging.debug("IdentityCredentialsProvides on_joined")
self.on.has_identity_credentials_clients.emit()
def _on_identity_credentials_relation_changed(self, event):
"""Handle IdentityCredentials changed."""
logging.debug("IdentityCredentials on_changed")
REQUIRED_KEYS = ['username']
values = [
event.relation.data[event.relation.app].get(k)
for k in REQUIRED_KEYS
]
# Validate data on the relation
if all(values):
username = event.relation.data[event.relation.app]['username']
self.on.ready_identity_credentials_clients.emit(
event.relation.id,
event.relation.name,
username,
)
def _on_identity_credentials_relation_broken(self, event):
"""Handle IdentityCredentials broken."""
logging.debug("IdentityCredentialsProvides on_departed")
self.on.identity_credentials_clients_gone.emit()
def set_identity_credentials(self, relation_name: int,
relation_id: str,
api_version: str,
auth_host: str,
auth_port: str,
auth_protocol: str,
internal_host: str,
internal_port: str,
internal_protocol: str,
credentials: str,
project_name: str,
project_id: str,
user_domain_name: str,
user_domain_id: str,
project_domain_name: str,
project_domain_id: str,
region: str,
admin_role: str):
logging.debug("Setting identity_credentials connection information.")
_identity_credentials_rel = None
for relation in self.framework.model.relations[relation_name]:
if relation.id == relation_id:
_identity_credentials_rel = relation
if not _identity_credentials_rel:
# Relation has disappeared so don't send the data
return
app_data = _identity_credentials_rel.data[self.charm.app]
app_data["api-version"] = api_version
app_data["auth-host"] = auth_host
app_data["auth-port"] = str(auth_port)
app_data["auth-protocol"] = auth_protocol
app_data["internal-host"] = internal_host
app_data["internal-port"] = str(internal_port)
app_data["internal-protocol"] = internal_protocol
app_data["credentials"] = credentials
app_data["project-name"] = project_name
app_data["project-id"] = project_id
app_data["user-domain-name"] = user_domain_name
app_data["user-domain-id"] = user_domain_id
app_data["project-domain-name"] = project_domain_name
app_data["project-domain-id"] = project_domain_id
app_data["region"] = region
app_data["internal-endpoint"] = self.charm.internal_endpoint
app_data["public-endpoint"] = self.charm.public_endpoint
app_data["admin-role"] = admin_role

View File

@ -0,0 +1,286 @@
"""RabbitMQProvides and Requires module.
This library contains the Requires and Provides classes for handling
the rabbitmq interface.
Import `RabbitMQRequires` in your charm, with the charm object and the
relation name:
- self
- "amqp"
Also provide two additional parameters to the charm object:
- username
- vhost
Two events are also available to respond to:
- connected
- ready
- goneaway
A basic example showing the usage of this relation follows:
```
from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires
class RabbitMQClientCharm(CharmBase):
def __init__(self, *args):
super().__init__(*args)
# RabbitMQ Requires
self.amqp = RabbitMQRequires(
self, "amqp",
username="myusername",
vhost="vhostname"
)
self.framework.observe(
self.amqp.on.connected, self._on_amqp_connected)
self.framework.observe(
self.amqp.on.ready, self._on_amqp_ready)
self.framework.observe(
self.amqp.on.goneaway, self._on_amqp_goneaway)
def _on_amqp_connected(self, event):
'''React to the RabbitMQ connected event.
This event happens when n RabbitMQ relation is added to the
model before credentials etc have been provided.
'''
# Do something before the relation is complete
pass
def _on_amqp_ready(self, event):
'''React to the RabbitMQ ready event.
The RabbitMQ interface will use the provided username and vhost for the
request to the rabbitmq server.
'''
# RabbitMQ Relation is ready. Do something with the completed relation.
pass
def _on_amqp_goneaway(self, event):
'''React to the RabbitMQ goneaway event.
This event happens when an RabbitMQ relation is removed.
'''
# RabbitMQ Relation has goneaway. shutdown services or suchlike
pass
```
"""
# The unique Charmhub library identifier, never change it
LIBID = "45622352791142fd9cf87232e3bd6f2a"
# Increment this major API version when introducing breaking changes
LIBAPI = 0
# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 1
import logging
from ops.framework import (
StoredState,
EventBase,
ObjectEvents,
EventSource,
Object,
)
from ops.model import Relation
from typing import List
logger = logging.getLogger(__name__)
class RabbitMQConnectedEvent(EventBase):
"""RabbitMQ connected Event."""
pass
class RabbitMQReadyEvent(EventBase):
"""RabbitMQ ready for use Event."""
pass
class RabbitMQGoneAwayEvent(EventBase):
"""RabbitMQ relation has gone-away Event"""
pass
class RabbitMQServerEvents(ObjectEvents):
"""Events class for `on`"""
connected = EventSource(RabbitMQConnectedEvent)
ready = EventSource(RabbitMQReadyEvent)
goneaway = EventSource(RabbitMQGoneAwayEvent)
class RabbitMQRequires(Object):
"""
RabbitMQRequires class
"""
on = RabbitMQServerEvents()
def __init__(self, charm, relation_name: str, username: str, vhost: str):
super().__init__(charm, relation_name)
self.charm = charm
self.relation_name = relation_name
self.username = username
self.vhost = vhost
self.framework.observe(
self.charm.on[relation_name].relation_joined,
self._on_amqp_relation_joined,
)
self.framework.observe(
self.charm.on[relation_name].relation_changed,
self._on_amqp_relation_changed,
)
self.framework.observe(
self.charm.on[relation_name].relation_departed,
self._on_amqp_relation_changed,
)
self.framework.observe(
self.charm.on[relation_name].relation_broken,
self._on_amqp_relation_broken,
)
def _on_amqp_relation_joined(self, event):
"""RabbitMQ relation joined."""
logging.debug("RabbitMQRabbitMQRequires on_joined")
self.on.connected.emit()
self.request_access(self.username, self.vhost)
def _on_amqp_relation_changed(self, event):
"""RabbitMQ relation changed."""
logging.debug("RabbitMQRabbitMQRequires on_changed/departed")
if self.password:
self.on.ready.emit()
def _on_amqp_relation_broken(self, event):
"""RabbitMQ relation broken."""
logging.debug("RabbitMQRabbitMQRequires on_broken")
self.on.goneaway.emit()
@property
def _amqp_rel(self) -> Relation:
"""The RabbitMQ relation."""
return self.framework.model.get_relation(self.relation_name)
@property
def password(self) -> str:
"""Return the RabbitMQ password from the server side of the relation."""
return self._amqp_rel.data[self._amqp_rel.app].get("password")
@property
def hostname(self) -> str:
"""Return the hostname from the RabbitMQ relation"""
return self._amqp_rel.data[self._amqp_rel.app].get("hostname")
@property
def ssl_port(self) -> str:
"""Return the SSL port from the RabbitMQ relation"""
return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port")
@property
def ssl_ca(self) -> str:
"""Return the SSL port from the RabbitMQ relation"""
return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca")
@property
def hostnames(self) -> List[str]:
"""Return a list of remote RMQ hosts from the RabbitMQ relation"""
_hosts = []
for unit in self._amqp_rel.units:
_hosts.append(self._amqp_rel.data[unit].get("ingress-address"))
return _hosts
def request_access(self, username: str, vhost: str) -> None:
"""Request access to the RabbitMQ server."""
if self.model.unit.is_leader():
logging.debug("Requesting RabbitMQ user and vhost")
self._amqp_rel.data[self.charm.app]["username"] = username
self._amqp_rel.data[self.charm.app]["vhost"] = vhost
class HasRabbitMQClientsEvent(EventBase):
"""Has RabbitMQClients Event."""
pass
class ReadyRabbitMQClientsEvent(EventBase):
"""RabbitMQClients Ready Event."""
pass
class RabbitMQClientEvents(ObjectEvents):
"""Events class for `on`"""
has_amqp_clients = EventSource(HasRabbitMQClientsEvent)
ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent)
class RabbitMQProvides(Object):
"""
RabbitMQProvides class
"""
on = RabbitMQClientEvents()
def __init__(self, charm, relation_name, callback):
super().__init__(charm, relation_name)
self.charm = charm
self.relation_name = relation_name
self.callback = callback
self.framework.observe(
self.charm.on[relation_name].relation_joined,
self._on_amqp_relation_joined,
)
self.framework.observe(
self.charm.on[relation_name].relation_changed,
self._on_amqp_relation_changed,
)
self.framework.observe(
self.charm.on[relation_name].relation_broken,
self._on_amqp_relation_broken,
)
def _on_amqp_relation_joined(self, event):
"""Handle RabbitMQ joined."""
logging.debug("RabbitMQRabbitMQProvides on_joined data={}"
.format(event.relation.data[event.relation.app]))
self.on.has_amqp_clients.emit()
def _on_amqp_relation_changed(self, event):
"""Handle RabbitMQ changed."""
logging.debug("RabbitMQRabbitMQProvides on_changed data={}"
.format(event.relation.data[event.relation.app]))
# Validate data on the relation
if self.username(event) and self.vhost(event):
self.on.ready_amqp_clients.emit()
if self.charm.unit.is_leader():
self.callback(event, self.username(event), self.vhost(event))
else:
logging.warning("Received RabbitMQ changed event without the "
"expected keys ('username', 'vhost') in the "
"application data bag. Incompatible charm in "
"other end of relation?")
def _on_amqp_relation_broken(self, event):
"""Handle RabbitMQ broken."""
logging.debug("RabbitMQRabbitMQProvides on_departed")
# TODO clear data on the relation
def username(self, event):
"""Return the RabbitMQ username from the client side of the relation."""
return event.relation.data[event.relation.app].get("username")
def vhost(self, event):
"""Return the RabbitMQ vhost from the client side of the relation."""
return event.relation.data[event.relation.app].get("vhost")

View File

@ -0,0 +1,46 @@
name: ceilometer-k8s
summary: OpenStack ceilometer service
maintainer: OpenStack Charmers <openstack-charmers@lists.ubuntu.com>
description: |
OpenStack ceilometer 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: 22.04/stable
assumes:
- k8s-api
- juju >= 3.2
tags:
- openstack
source: https://opendev.org/openstack/charm-ceilometer-k8s
issues: https://bugs.launchpad.net/charm-ceilometer-k8s
containers:
ceilometer-central:
resource: ceilometer-central-image
ceilometer-notification:
resource: ceilometer-notification-image
resources:
ceilometer-central-image:
type: oci-image
description: OCI image for OpenStack ceilometer
upstream-source: kolla/ubuntu-binary-ceilometer-central:yoga
ceilometer-notification-image:
type: oci-image
description: OCI image for OpenStack ceilometer
upstream-source: kolla/ubuntu-binary-ceilometer-notification:yoga
requires:
amqp:
interface: rabbitmq
identity-credentials:
interface: keystone-credentials
limit: 1
peers:
peers:
interface: ceilometer-peer

View File

@ -0,0 +1,10 @@
- project:
templates:
- charm-publish-jobs
vars:
needs_charm_build: true
charm_build_name: ceilometer-k8s
build_type: charmcraft
publish_charm: true
charmcraft_channel: 2.0/stable
publish_channel: 2023.1/edge

View File

@ -0,0 +1,33 @@
# Testing tools configuration
[tool.coverage.run]
branch = true
[tool.coverage.report]
show_missing = true
[tool.pytest.ini_options]
minversion = "6.0"
log_cli_level = "INFO"
# Formatting tools configuration
[tool.black]
line-length = 99
target-version = ["py38"]
[tool.isort]
line_length = 99
profile = "black"
# Linting tools configuration
[tool.flake8]
max-line-length = 99
max-doc-length = 99
max-complexity = 10
exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
select = ["E", "W", "F", "C", "N", "R", "D", "H"]
# Ignore W503, E501 because using black creates errors with this
# Ignore D107 Missing docstring in __init__
ignore = ["W503", "E501", "D107"]
# D100, D101, D102, D103: Ignore missing docstrings in tests
per-file-ignores = ["tests/*:D100,D101,D102,D103,D104"]
docstring-convention = "google"

13
charms/ceilometer-k8s/rename.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash
charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
echo "renaming ${charm}_*.charm to ${charm}.charm"
echo -n "pwd: "
pwd
ls -al
echo "Removing bad downloaded charm maybe?"
if [[ -e "${charm}.charm" ]];
then
rm "${charm}.charm"
fi
echo "Renaming charm here."
mv ${charm}_*.charm ${charm}.charm

View File

@ -0,0 +1,8 @@
ops
jinja2
git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam
lightkube
# Uncomment below if charm relates to ceph
# git+https://github.com/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client
# git+https://github.com/juju/charm-helpers.git#egg=charmhelpers

View File

@ -0,0 +1,141 @@
#!/usr/bin/env python3
"""Ceilometer Operator Charm.
This charm provide Ceilometer services as part of an OpenStack deployment
"""
import logging
import uuid
from typing import List
import ops.framework
import ops_sunbeam.charm as sunbeam_charm
import ops_sunbeam.container_handlers as container_handlers
import ops_sunbeam.core as core
from ops.main import main
logger = logging.getLogger(__name__)
CEILOMETER_CENTRAL_CONTAINER = "ceilometer-central"
CEILOMETER_NOTIFICATION_CONTAINER = "ceilometer-notification"
class CeilometerCentralPebbleHandler(container_handlers.ServicePebbleHandler):
"""Pebble handler for ceilometer-central service."""
def get_layer(self) -> dict:
"""ceilometer-central service pebble layer.
:returns: pebble layer configuration for ceilometer-central service
:rtype: dict
"""
return {
"summary": "ceilometer-central layer",
"description": "pebble config layer for ceilometer-central service",
"services": {
"ceilometer-central": {
"override": "replace",
"summary": "ceilometer-central",
"command": "/usr/bin/ceilometer-polling --config-file=/etc/ceilometer/ceilometer.conf --polling-namespaces central --use-syslog",
"startup": "enabled",
"user": "ceilometer",
"group": "ceilometer",
},
},
}
class CeilometerNotificationPebbleHandler(container_handlers.ServicePebbleHandler):
"""Pebble handler for ceilometer-notification service."""
def get_layer(self) -> dict:
"""ceilometer-notification service pebble layer.
:returns: pebble layer configuration for ceilometer-notification service
:rtype: dict
"""
return {
"summary": "ceilometer-notification layer",
"description": "pebble config layer for ceilometer-notification service",
"services": {
"ceilometer-notification": {
"override": "replace",
"summary": "ceilometer-notification",
"command": "/usr/bin/ceilometer-agent-notification --config-file=/etc/ceilometer/ceilometer.conf --use-syslog",
"startup": "enabled",
"user": "ceilometer",
"group": "ceilometer",
},
},
}
class CeilometerOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S):
"""Charm the service."""
service_name = "ceilometer"
shared_metering_secret_key = "shared-metering-secret"
mandatory_relations = {"amqp", "identity-credentials"}
def get_shared_meteringsecret(self):
"""Return the shared metering secret."""
return self.leader_get(self.shared_metering_secret_key)
def set_shared_meteringsecret(self):
"""Store the shared metering secret."""
self.leader_set({self.shared_metering_secret_key: str(uuid.uuid1())})
def configure_charm(self, event: ops.framework.EventBase) -> None:
"""Callback handler for nova operator configuration."""
if not self.peers.ready:
return
metering_secret = self.get_shared_meteringsecret()
if metering_secret:
logger.debug("Found metering secret in leader DB")
else:
if self.unit.is_leader():
logger.debug("Creating metering secret")
self.set_shared_meteringsecret()
else:
logger.debug("Metadata secret not ready")
return
super().configure_charm(event)
@property
def container_configs(self) -> List[core.ContainerConfigFile]:
"""Container configurations for the operator."""
_cconfigs = [
core.ContainerConfigFile(
"/etc/ceilometer/ceilometer.conf",
"root",
"ceilometer",
0o640,
),
]
return _cconfigs
def get_pebble_handlers(self) -> List[container_handlers.PebbleHandler]:
"""Pebble handlers for the operator."""
return [
CeilometerCentralPebbleHandler(
self,
CEILOMETER_CENTRAL_CONTAINER,
"ceilometer-central",
self.container_configs,
self.template_dir,
self.configure_charm,
),
CeilometerNotificationPebbleHandler(
self,
CEILOMETER_NOTIFICATION_CONTAINER,
"ceilometer-notification",
self.container_configs,
self.template_dir,
self.configure_charm,
),
]
if __name__ == "__main__":
main(CeilometerOperatorCharm)

View File

@ -0,0 +1,31 @@
[DEFAULT]
debug = {{ options.debug }}
# event_pipeline_cfg_file = /etc/ceilometer/event_pipeline.yaml
meter_dispatchers = gnocchi
event_dispatchers = gnocchi
{% if amqp.transport_url -%}
transport_url = {{ amqp.transport_url }}
{%- endif %}
[notification]
{% if amqp.transport_url -%}
messaging_urls = {{ amqp.transport_url }}
{% endif %}
[polling]
batch_size = 50
[publisher]
telemetry_secret = {{ peers.shared_metering_secret }}
[gnocchi]
filter_service_activity = False
archive_policy = low
[keystone_authtoken]
{% include "parts/identity-data-id-creds" %}
{% include "parts/section-service-user-id-creds" %}

View File

@ -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 %}

View File

@ -0,0 +1,3 @@
{% if database.connection -%}
connection = {{ database.connection }}
{% endif -%}

View File

@ -0,0 +1,23 @@
{% if identity_service.admin_auth_url -%}
auth_url = {{ identity_service.admin_auth_url }}
interface = admin
{% elif identity_service.internal_auth_url -%}
auth_url = {{ identity_service.internal_auth_url }}
interface = internal
{% elif identity_service.internal_host -%}
auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
interface = internal
{% endif -%}
{% if identity_service.public_auth_url -%}
www_authenticate_uri = {{ identity_service.public_auth_url }}
{% elif identity_service.internal_host -%}
www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
{% endif -%}
auth_type = password
project_domain_name = {{ identity_service.service_domain_name }}
user_domain_name = {{ identity_service.service_domain_name }}
project_name = {{ identity_service.service_project_name }}
username = {{ identity_service.service_user_name }}
password = {{ identity_service.service_password }}
service_token_roles = {{ identity_service.admin_role }}
service_token_roles_required = True

View File

@ -0,0 +1,23 @@
{% if identity_credentials.admin_auth_url -%}
auth_url = {{ identity_credentials.admin_auth_url }}
interface = admin
{% elif identity_credentials.internal_auth_url -%}
auth_url = {{ identity_credentials.internal_auth_url }}
interface = internal
{% elif identity_credentials.internal_host -%}
auth_url = {{ identity_credentials.internal_protocol }}://{{ identity_credentials.internal_host }}:{{ identity_credentials.internal_port }}
interface = internal
{% endif -%}
{% if identity_credentials.public_auth_url -%}
www_authenticate_uri = {{ identity_credentials.public_auth_url }}
{% elif identity_credentials.internal_host -%}
www_authenticate_uri = {{ identity_credentials.internal_protocol }}://{{ identity_credentials.internal_host }}:{{ identity_credentials.internal_port }}
{% endif -%}
auth_type = password
project_domain_name = {{ identity_credentials.project_domain_name }}
user_domain_name = {{ identity_credentials.user_domain_name }}
project_name = {{ identity_credentials.project_name }}
username = {{ identity_credentials.username }}
password = {{ identity_credentials.password }}
service_token_roles = {{ identity_credentials.admin_role }}
service_token_roles_required = True

View File

@ -0,0 +1,3 @@
[database]
{% include "parts/database-connection" %}
connection_recycle_time = 200

View File

@ -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 -%}

View File

@ -0,0 +1,2 @@
[keystone_authtoken]
{% include "parts/identity-data" %}

View File

@ -0,0 +1,6 @@
{% for section in sections -%}
[{{section}}]
{% for key, value in sections[section].items() -%}
{{ key }} = {{ value }}
{% endfor %}
{%- endfor %}

View File

@ -0,0 +1,15 @@
{% if identity_service.service_domain_id -%}
[service_user]
{% if identity_service.internal_auth_url -%}
auth_url = {{ identity_service.internal_auth_url }}
{% elif identity_service.internal_host -%}
auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
{% endif -%}
send_service_user_token = true
auth_type = password
project_domain_id = {{ identity_service.service_domain_id }}
user_domain_id = {{ identity_service.service_domain_id }}
project_name = {{ identity_service.service_project_name }}
username = {{ identity_service.service_user_name }}
password = {{ identity_service.service_password }}
{% endif -%}

View File

@ -0,0 +1,15 @@
{% if identity_credentials.project_domain_id -%}
[service_user]
{% if identity_credentials.internal_auth_url -%}
auth_url = {{ identity_credentials.internal_auth_url }}
{% elif identity_credentials.internal_host -%}
auth_url = {{ identity_credentials.internal_protocol }}://{{ identity_credentials.internal_host }}:{{ identity_credentials.internal_port }}
{% endif -%}
send_service_user_token = true
auth_type = password
project_domain_id = {{ identity_credentials.project_domain_id }}
user_domain_id = {{ identity_credentials.user_domain_id }}
project_name = {{ identity_credentials.project_name }}
username = {{ identity_credentials.username }}
password = {{ identity_credentials.password }}
{% endif -%}

View File

@ -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 -%}

View File

@ -0,0 +1,28 @@
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_public.ingress_path -%}
WSGIScriptAlias {{ ingress_public.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>

View File

@ -0,0 +1,28 @@
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_public.ingress_path -%}
WSGIScriptAlias {{ ingress_public.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>

View File

@ -0,0 +1,9 @@
# This file is managed centrally. If you find the need to modify this as a
# one-off, please don't. Intead, consult #openstack-charms and ask about
# requirements management in charms via bot-control. Thank you.
coverage
mock
flake8
stestr
ops

View File

@ -0,0 +1 @@
../config.yaml

View File

@ -0,0 +1,35 @@
#!/usr/bin/env python3
# Copyright 2023 liam
# 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(await charm, resources=resources, application_name=APP_NAME),
ops_test.model.wait_for_idle(
apps=[APP_NAME], status="active", raise_on_blocked=True, timeout=1000
),
)

View File

@ -0,0 +1,18 @@
gate_bundles:
- smoke
smoke_bundles:
- smoke
configure:
- zaza.openstack.charm_tests.keystone.setup.add_tempest_roles
tests: []
tests_options:
trust:
- smoke
ignore_hard_deploy_errors:
- smoke
tempest:
default:
smoke: True
target_deploy_status: []

View File

@ -0,0 +1,17 @@
#!/usr/bin/env python3
# Copyright 2023 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unit tests for ceilometer operator."""

View File

@ -0,0 +1,65 @@
#!/usr/bin/env python3
# Copyright 2021 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for gnocchi charm."""
import ops_sunbeam.test_utils as test_utils
import charm
class _CeilometerOperatorCharm(charm.CeilometerOperatorCharm):
def __init__(self, framework):
self.seen_events = []
super().__init__(framework)
def _log_event(self, event):
self.seen_events.append(type(event).__name__)
def configure_charm(self, event):
super().configure_charm(event)
self._log_event(event)
class TestCeilometerOperatorCharm(test_utils.CharmTestCase):
"""Class for testing gnocchi charm."""
PATCHES = []
def setUp(self):
"""Run setup for unit tests."""
super().setUp(charm, self.PATCHES)
self.harness = test_utils.get_harness(
_CeilometerOperatorCharm, container_calls=self.container_calls
)
def test_pebble_ready_handler(self):
"""Test Pebble ready event is captured."""
self.harness.begin()
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 the charms relations."""
self.harness.begin_with_initial_hooks()
self.harness.set_leader()
test_utils.set_all_pebbles_ready(self.harness)
test_utils.add_complete_identity_credentials_relation(self.harness)
test_utils.add_complete_amqp_relation(self.harness)
for c in ["ceilometer-central", "ceilometer-notification"]:
self.check_file(c, "/etc/ceilometer/ceilometer.conf")

View File

@ -0,0 +1,161 @@
# Operator charm (with zaza): tox.ini
[tox]
skipsdist = True
envlist = pep8,py3
sitepackages = False
skip_missing_interpreters = False
minversion = 3.18.0
[vars]
src_path = {toxinidir}/src/
tst_path = {toxinidir}/tests/
lib_path = {toxinidir}/lib/
pyproject_toml = {toxinidir}/pyproject.toml
all_path = {[vars]src_path} {[vars]tst_path}
[testenv]
basepython = python3
setenv =
PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
passenv =
HOME
PYTHONPATH
install_command =
pip install {opts} {packages}
commands = stestr run --slowest {posargs}
allowlist_externals =
git
charmcraft
{toxinidir}/fetch-libs.sh
{toxinidir}/rename.sh
deps =
-r{toxinidir}/test-requirements.txt
[testenv:fmt]
description = Apply coding style standards to code
deps =
black
isort
commands =
isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
[testenv:build]
basepython = python3
deps =
commands =
charmcraft -v pack
{toxinidir}/rename.sh
[testenv:fetch]
basepython = python3
deps =
commands =
{toxinidir}/fetch-libs.sh
[testenv:py3]
basepython = python3
deps =
{[testenv]deps}
-r{toxinidir}/requirements.txt
[testenv:py38]
basepython = python3.8
deps = {[testenv:py3]deps}
[testenv:py39]
basepython = python3.9
deps = {[testenv:py3]deps}
[testenv:py310]
basepython = python3.10
deps = {[testenv:py3]deps}
[testenv:cover]
basepython = python3
deps = {[testenv:py3]deps}
setenv =
{[testenv]setenv}
PYTHON=coverage run
commands =
coverage erase
stestr run --slowest {posargs}
coverage combine
coverage html -d cover
coverage xml -o cover/coverage.xml
coverage report
[testenv:pep8]
description = Alias for lint
deps = {[testenv:lint]deps}
commands = {[testenv:lint]commands}
[testenv:lint]
description = Check code against coding style standards
deps =
black
flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged
flake8-docstrings
flake8-copyright
flake8-builtins
pyproject-flake8
pep8-naming
isort
codespell
commands =
codespell {[vars]all_path}
# pflake8 wrapper supports config from pyproject.toml
pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
[testenv:func-noop]
basepython = python3
deps =
git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
git+https://opendev.org/openstack/tempest.git#egg=tempest
commands =
functest-run-suite --help
[testenv:func]
basepython = python3
deps = {[testenv:func-noop]deps}
commands =
functest-run-suite --keep-model
[testenv:func-smoke]
basepython = python3
deps = {[testenv:func-noop]deps}
setenv =
TEST_MODEL_SETTINGS = automatically-retry-hooks=true
TEST_MAX_RESOLVE_COUNT = 5
commands =
functest-run-suite --keep-model --smoke
[testenv:func-dev]
basepython = python3
deps = {[testenv:func-noop]deps}
commands =
functest-run-suite --keep-model --dev
[testenv:func-target]
basepython = python3
deps = {[testenv:func-noop]deps}
commands =
functest-run-suite --keep-model --bundle {posargs}
[coverage:run]
branch = True
concurrency = multiprocessing
parallel = True
source =
.
omit =
.tox/*
tests/*
src/templates/*
[flake8]
ignore=E226,W504