Switch to using aso

This commit is contained in:
Liam Young 2021-10-12 13:27:22 +01:00
parent 15e6d6536d
commit 3b13d29aed
18 changed files with 946 additions and 92 deletions

View File

@ -1,7 +1,9 @@
venv/
build/
.stestr/
*.charm
.tox
.coverage
__pycache__/
*.py[cod]
lib/charms/sunbeam_rabbitmq_operator/

View File

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

View File

@ -0,0 +1,2 @@
git+https://github.com/canonical/charmcraft.git@0.10.2#egg=charmcraft

View File

@ -0,0 +1,470 @@
"""IdentityServiceProvides and Requires module.
This library contains the Requires and Provides classes for handling
the identity_service interface.
Import `IdentityServiceRequires` in your charm, with the charm object and the
relation name:
- self
- "identity_service"
Also provide additional parameters to the charm object:
- service
- internal_url
- public_url
- admin_url
- region
- username
- vhost
Two events are also available to respond to:
- connected
- ready
- goneaway
A basic example showing the usage of this relation follows:
```
from charms.sunbeam_sunbeam_identity_service_operator.v0.identity_service import IdentityServiceRequires
class IdentityServiceClientCharm(CharmBase):
def __init__(self, *args):
super().__init__(*args)
# IdentityService Requires
self.identity_service = IdentityServiceRequires(
self, "identity_service",
service = "my-service"
internal_url = "http://internal-url"
public_url = "http://public-url"
admin_url = "http://admin-url"
region = "region"
)
self.framework.observe(
self.identity_service.on.connected, self._on_identity_service_connected)
self.framework.observe(
self.identity_service.on.ready, self._on_identity_service_ready)
self.framework.observe(
self.identity_service.on.goneaway, self._on_identity_service_goneaway)
def _on_identity_service_connected(self, event):
'''React to the IdentityService connected event.
This event happens when n IdentityService relation is added to the
model before credentials etc have been provided.
'''
# Do something before the relation is complete
pass
def _on_identity_service_ready(self, event):
'''React to the IdentityService ready event.
The IdentityService interface will use the provided config for the
request to the identity server.
'''
# IdentityService Relation is ready. Do something with the completed relation.
pass
def _on_identity_service_goneaway(self, event):
'''React to the IdentityService goneaway event.
This event happens when an IdentityService relation is removed.
'''
# IdentityService Relation has goneaway. shutdown services or suchlike
pass
```
"""
# The unique Charmhub library identifier, never change it
# LIBID = ""
# 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
import json
import logging
import requests
from ops.framework import (
StoredState,
EventBase,
ObjectEvents,
EventSource,
Object,
)
from ops.model import Relation
from typing import List
logger = logging.getLogger(__name__)
class IdentityServiceConnectedEvent(EventBase):
"""IdentityService connected Event."""
pass
class IdentityServiceReadyEvent(EventBase):
"""IdentityService ready for use Event."""
pass
class IdentityServiceGoneAwayEvent(EventBase):
"""IdentityService relation has gone-away Event"""
pass
class IdentityServiceServerEvents(ObjectEvents):
"""Events class for `on`"""
connected = EventSource(IdentityServiceConnectedEvent)
ready = EventSource(IdentityServiceReadyEvent)
goneaway = EventSource(IdentityServiceGoneAwayEvent)
class IdentityServiceRequires(Object):
"""
IdentityServiceRequires class
"""
on = IdentityServiceServerEvents()
_stored = StoredState()
def __init__(self, charm, relation_name: str, service_endpoints: dict,
region: str):
super().__init__(charm, relation_name)
self.charm = charm
self.relation_name = relation_name
self.service_endpoints = service_endpoints
self.region = region
self.framework.observe(
self.charm.on[relation_name].relation_joined,
self._on_identity_service_relation_joined,
)
self.framework.observe(
self.charm.on[relation_name].relation_changed,
self._on_identity_service_relation_changed,
)
self.framework.observe(
self.charm.on[relation_name].relation_departed,
self._on_identity_service_relation_changed,
)
self.framework.observe(
self.charm.on[relation_name].relation_broken,
self._on_identity_service_relation_broken,
)
def _on_identity_service_relation_joined(self, event):
"""IdentityService relation joined."""
logging.debug("IdentityService on_joined")
self.on.connected.emit()
self.register_services(
self.service_endpoints,
self.region)
def _on_identity_service_relation_changed(self, event):
"""IdentityService relation changed."""
logging.debug("IdentityService on_changed")
try:
self.service_password
self.on.ready.emit()
except AttributeError:
pass
def _on_identity_service_relation_broken(self, event):
"""IdentityService relation broken."""
logging.debug("IdentityService on_broken")
self.on.goneaway.emit()
@property
def _identity_service_rel(self) -> Relation:
"""The IdentityService relation."""
return self.framework.model.get_relation(self.relation_name)
def get_remote_app_data(self, key: str) -> str:
"""Return the value for the given key from remote app data."""
data = self._identity_service_rel.data[self._identity_service_rel.app]
return data.get(key)
@property
def api_version(self) -> str:
"""Return the api_version."""
return self.get_remote_app_data('api-version')
@property
def auth_host(self) -> str:
"""Return the auth_host."""
return self.get_remote_app_data('auth-host')
@property
def auth_port(self) -> str:
"""Return the auth_port."""
return self.get_remote_app_data('auth-port')
@property
def auth_protocol(self) -> str:
"""Return the auth_protocol."""
return self.get_remote_app_data('auth-protocol')
@property
def internal_host(self) -> str:
"""Return the internal_host."""
return self.get_remote_app_data('internal-host')
@property
def internal_port(self) -> str:
"""Return the internal_port."""
return self.get_remote_app_data('internal-port')
@property
def internal_protocol(self) -> str:
"""Return the internal_protocol."""
return self.get_remote_app_data('internal-protocol')
@property
def admin_domain_name(self) -> str:
"""Return the admin_domain_name."""
return self.get_remote_app_data('admin-domain-name')
@property
def admin_domain_id(self) -> str:
"""Return the admin_domain_id."""
return self.get_remote_app_data('admin-domain-id')
@property
def admin_project_name(self) -> str:
"""Return the admin_project_name."""
return self.get_remote_app_data('admin-project-name')
@property
def admin_project_id(self) -> str:
"""Return the admin_project_id."""
return self.get_remote_app_data('admin-project-id')
@property
def admin_user_name(self) -> str:
"""Return the admin_user_name."""
return self.get_remote_app_data('admin-user-name')
@property
def admin_user_id(self) -> str:
"""Return the admin_user_id."""
return self.get_remote_app_data('admin-user-id')
@property
def service_domain_name(self) -> str:
"""Return the service_domain_name."""
return self.get_remote_app_data('service-domain-name')
@property
def service_domain_id(self) -> str:
"""Return the service_domain_id."""
return self.get_remote_app_data('service-domain-id')
@property
def service_host(self) -> str:
"""Return the service_host."""
return self.get_remote_app_data('service-host')
@property
def service_password(self) -> str:
"""Return the service_password."""
return self.get_remote_app_data('service-password')
@property
def service_port(self) -> str:
"""Return the service_port."""
return self.get_remote_app_data('service-port')
@property
def service_protocol(self) -> str:
"""Return the service_protocol."""
return self.get_remote_app_data('service-protocol')
@property
def service_project_name(self) -> str:
"""Return the service_project_name."""
return self.get_remote_app_data('service-project-name')
@property
def service_project_id(self) -> str:
"""Return the service_project_id."""
return self.get_remote_app_data('service-project-id')
@property
def service_user_name(self) -> str:
"""Return the service_user_name."""
return self.get_remote_app_data('service-user-name')
@property
def service_user_id(self) -> str:
"""Return the service_user_id."""
return self.get_remote_app_data('service-user-id')
def register_services(self, service_endpoints: dict,
region: str) -> None:
"""Request access to the IdentityService server."""
if self.model.unit.is_leader():
logging.debug("Requesting service registration")
app_data = self._identity_service_rel.data[self.charm.app]
app_data["service-endpoints"] = json.dumps(service_endpoints)
app_data["region"] = region
class HasIdentityServiceClientsEvent(EventBase):
"""Has IdentityServiceClients Event."""
pass
class ReadyIdentityServiceClientsEvent(EventBase):
"""IdentityServiceClients Ready Event."""
def __init__(self, handle, relation_id, relation_name, service_endpoints,
region, client_app_name):
super().__init__(handle)
self.relation_id = relation_id
self.relation_name = relation_name
self.service_endpoints = service_endpoints
self.region = region
self.client_app_name = client_app_name
def snapshot(self):
return {
"relation_id": self.relation_id,
"relation_name": self.relation_name,
"service_endpoints": self.service_endpoints,
"client_app_name": self.client_app_name,
"region": self.region}
def restore(self, snapshot):
super().restore(snapshot)
self.relation_id = snapshot["relation_id"]
self.relation_name = snapshot["relation_name"]
self.service_endpoints = snapshot["service_endpoints"]
self.region = snapshot["region"]
self.client_app_name = snapshot["client_app_name"]
class IdentityServiceClientEvents(ObjectEvents):
"""Events class for `on`"""
has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent)
ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent)
class IdentityServiceProvides(Object):
"""
IdentityServiceProvides class
"""
on = IdentityServiceClientEvents()
_stored = StoredState()
def __init__(self, charm, relation_name):
super().__init__(charm, relation_name)
self.charm = charm
self.relation_name = relation_name
self.framework.observe(
self.charm.on[relation_name].relation_joined,
self._on_identity_service_relation_joined,
)
self.framework.observe(
self.charm.on[relation_name].relation_changed,
self._on_identity_service_relation_changed,
)
self.framework.observe(
self.charm.on[relation_name].relation_broken,
self._on_identity_service_relation_broken,
)
def _on_identity_service_relation_joined(self, event):
"""Handle IdentityService joined."""
logging.debug("IdentityService on_joined")
self.on.has_identity_service_clients.emit()
def _on_identity_service_relation_changed(self, event):
"""Handle IdentityService changed."""
logging.debug("IdentityService on_changed")
REQUIRED_KEYS = [
'service-endpoints',
'region']
values = [
event.relation.data[event.relation.app].get(k)
for k in REQUIRED_KEYS ]
# Validate data on the relation
if all(values):
print(event.relation.id)
print(event.relation.name)
service_eps = json.loads(
event.relation.data[event.relation.app]['service-endpoints'])
self.on.ready_identity_service_clients.emit(
event.relation.id,
event.relation.name,
service_eps,
event.relation.data[event.relation.app]['region'],
event.relation.app.name)
def _on_identity_service_relation_broken(self, event):
"""Handle IdentityService broken."""
logging.debug("IdentityServiceProvides on_departed")
# TODO clear data on the relation
def set_identity_service_credentials(self, relation_name: int,
relation_id: str,
api_version: str,
auth_host: str,
auth_port: str,
auth_protocol: str,
internal_host: str,
internal_port: str,
internal_protocol: str,
service_host: str,
service_port: str,
service_protocol: str,
admin_domain: str,
admin_project: str,
admin_user: str,
service_domain: str,
service_password: str,
service_project: str,
service_user: str):
logging.debug("Setting identity_service connection information.")
for relation in self.framework.model.relations[relation_name]:
if relation.id == relation_id:
_identity_service_rel = relation
app_data = _identity_service_rel.data[self.charm.app]
app_data["api-version"] = api_version
app_data["auth-host"] = auth_host
app_data["auth-port"] = str(auth_port)
app_data["auth-protocol"] = auth_protocol
app_data["internal-host"] = internal_host
app_data["internal-port"] = str(internal_port)
app_data["internal-protocol"] = internal_protocol
app_data["service-host"] = service_host
app_data["service-port"] = str(service_port)
app_data["service-protocol"] = service_protocol
app_data["admin-domain-name"] = admin_domain.name
app_data["admin-domain-id"] = admin_domain.id
app_data["admin-project-name"] = admin_project.name
app_data["admin-project-id"] = admin_project.id
app_data["admin-user-name"] = admin_user.name
app_data["admin-user-id"] = admin_user.id
app_data["service-domain-name"] = service_domain.name
app_data["service-domain-id"] = service_domain.id
app_data["service-project-name"] = service_project.name
app_data["service-project-id"] = service_project.id
app_data["service-user-name"] = service_user.name
app_data["service-user-id"] = service_user.id
app_data["service-password"] = service_password

View File

@ -17,14 +17,17 @@ tags:
containers:
cinder-api:
resource: cinder-base
resource: cinder-api-image
cinder-scheduler:
resource: cinder-base
resource: cinder-scheduler-image
resources:
cinder-base:
cinder-api-image:
type: oci-image
description: OCI image for OpenStack Cinder (kolla/cinder-base)
description: OCI image for OpenStack Cinder (kolla/cinder-api-image)
cinder-scheduler-image:
type: oci-image
description: OCI image for OpenStack Cinder (kolla/cinder-api-image)
requires:
cinder-db:

View File

@ -2,3 +2,5 @@
jinja2
git+https://github.com/canonical/operator@2875e73e#egg=ops
git+https://opendev.org/openstack/charm-ops-openstack#egg=ops_openstack
# git+https://github.com/openstack-charmers/advanced-sunbeam-openstack#egg=advanced_sunbeam_openstack
git+https://github.com/gnuoy/advanced-sunbeam-openstack@identity-service#egg=advanced_sunbeam_openstack

View File

@ -6,79 +6,61 @@ This charm provide Cinder services as part of an OpenStack deployment
import logging
from charms.nginx_ingress_integrator.v0.ingress import IngressRequires
from charms.mysql.v1.mysql import MySQLConsumer
from ops.charm import CharmBase
from ops.framework import StoredState
from ops.main import main
from ops.model import ActiveStatus
import advanced_sunbeam_openstack.cprocess as sunbeam_cprocess
import advanced_sunbeam_openstack.charm as sunbeam_charm
import advanced_sunbeam_openstack.core as sunbeam_core
import advanced_sunbeam_openstack.container_handlers as sunbeam_chandlers
logger = logging.getLogger(__name__)
CINDER_API_PORT = 8090
CINDER_API_CONTAINER = "cinder-api"
CINDER_SCHEDULER_CONTAINER = "cinder-scheduler"
class CinderOperatorCharm(CharmBase):
"""Charm the service."""
class CinderWSGIPebbleHandler(sunbeam_chandlers.WSGIPebbleHandler):
_stored = StoredState()
def init_service(self, context) -> None:
"""Enable and start WSGI service"""
container = self.charm.unit.get_container(self.container_name)
self.write_config(context)
try:
sunbeam_cprocess.check_output(
container,
(f'a2disconf cinder-wsgi; a2ensite {self.wsgi_service_name} '
'&& sleep 1'))
except sunbeam_cprocess.ContainerProcessError:
logger.exception(
f'Failed to enable {self.wsgi_service_name} site in apache')
# ignore for now - pebble is raising an exited too quickly, but it
# appears to work properly.
self.start_wsgi()
self._state.service_ready = True
def __init__(self, *args):
super().__init__(*args)
self.framework.observe(self.on.cinder_api_pebble_ready,
self._on_cinder_api_pebble_ready)
self.framework.observe(self.on.cinder_scheduler_pebble_ready,
self._on_cinder_scheduler_pebble_ready)
class CinderSchedulerPebbleHandler(sunbeam_chandlers.PebbleHandler):
self.framework.observe(self.on.config_changed,
self._on_config_changed)
def start_service(self):
container = self.charm.unit.get_container(self.container_name)
if not container:
logger.debug(f'{self.container_name} container is not ready. '
'Cannot start service.')
return
service = container.get_service(self.service_name)
if service.is_running():
container.stop(self.service_name)
# Register the database consumer and register for events
self.db = MySQLConsumer(self, 'cinder-db', {"mysql": ">=8"})
self.framework.observe(self.on.cinder_db_relation_changed,
self._on_database_changed)
container.start(self.service_name)
# Access to API service from outside of K8S
self.ingress_public = IngressRequires(self, {
'service-hostname': self.model.config['os-public-hostname'],
'service-name': self.app.name,
'service-port': CINDER_API_PORT,
})
def get_layer(self):
"""Apache service
self._stored.set_default(db_ready=False)
self._stored.set_default(amqp_ready=False)
self._stored.set_default(identity_ready=False)
# TODO
# Register AMQP consumer + events
# Register Identity Service consumer + events
# TODO
# State modelling
# DB & AMQP & Identity -> API and Scheduler
# Store URL's etc on _changed events?
@property
def _pebble_cinder_api_layer(self):
"""Pebble layer for Cinder API"""
return {
"summary": "cinder layer",
"description": "pebble configuration for cinder services",
"services": {
"cinder-api": {
"override": "replace",
"summary": "Cinder API",
"command": "/usr/sbin/apache2ctl -DFOREGROUND",
"startup": "enabled"
}
}
}
@property
def _pebble_cinder_scheduler_layer(self):
"""Pebble layer for Cinder Scheduler"""
:returns: pebble layer configuration for wsgi services
:rtype: dict
"""
return {
"summary": "cinder layer",
"description": "pebble configuration for cinder services",
@ -92,38 +74,123 @@ class CinderOperatorCharm(CharmBase):
}
}
def _on_cinder_api_pebble_ready(self, event):
"""Define and start a workload using the Pebble API."""
container = event.workload
container.add_layer("cinder-api", self._pebble_cinder_api_layer, combine=True)
container.autostart()
def init_service(self, context):
self.write_config(context)
# container = self.charm.unit.get_container(self.container_name)
# try:
# sunbeam_cprocess.check_output(
# container,
# f'a2ensite {self.wsgi_service_name} && sleep 1')
# except sunbeam_cprocess.ContainerProcessError:
# logger.exception(
# f'Failed to enable {self.wsgi_service_name} site in apache')
# # ignore for now - pebble is raising an exited too quickly, but it
# # appears to work properly.
self.start_service()
self._state.service_ready = True
def _on_cinder_scheduler_pebble_ready(self, event):
"""Define and start a workload using the Pebble API."""
container = event.workload
container.add_layer("cinder-scheduler", self._pebble_cinder_scheduler_layer, combine=True)
container.autostart()
def default_container_configs(self):
return [
sunbeam_core.ContainerConfigFile(
[self.container_name],
'/etc/cinder/cinder.conf',
'cinder',
'cinder')]
def _on_config_changed(self, _):
"""Just an example to show how to deal with changed configuration"""
# TODO
# Set debug logging and restart services
pass
def _on_database_ready(self, event):
"""Database ready for use"""
# TODO
# Run sync process if on lead unit
pass
class CinderOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
"""Charm the service."""
def _on_amqp_ready(self, event):
"""AMQP service ready for use"""
pass
_state = StoredState()
_authed = False
service_name = "cinder"
wsgi_admin_script = '/usr/bin/cinder-wsgi-admin'
wsgi_public_script = '/usr/bin/cinder-wsgi-public'
def _on_identity_service_ready(self, event):
"""Identity service ready for use"""
pass
def __init__(self, framework):
super().__init__(framework)
self._state.set_default(admin_domain_name='admin_domain')
self._state.set_default(admin_domain_id=None)
self._state.set_default(default_domain_id=None)
self._state.set_default(service_project_id=None)
@property
def service_endpoints(self):
return [
{
'service_name': 'cinderv2',
'type': 'volumev2',
'description': "Cinder Volume Service v2",
'internal_url': f'{self.internal_url}/v2/$(tenant_id)s',
'public_url': f'{self.public_url}/v2/$(tenant_id)s',
'admin_url': f'{self.admin_url}/v2/$(tenant_id)s'},
{
'service_name': 'cinderv3',
'type': 'volumev3',
'description': "Cinder Volume Service v3",
'internal_url': f'{self.internal_url}/v3/$(tenant_id)s',
'public_url': f'{self.public_url}/v3/$(tenant_id)s',
'admin_url': f'{self.admin_url}/v3/$(tenant_id)s'}]
def get_pebble_handlers(self):
pebble_handlers = [
CinderWSGIPebbleHandler(
self,
CINDER_API_CONTAINER,
self.service_name,
self.container_configs,
self.template_dir,
self.openstack_release,
self.configure_charm,
f'wsgi-{self.service_name}'),
CinderSchedulerPebbleHandler(
self,
CINDER_SCHEDULER_CONTAINER,
'cinder-scheduler',
[],
self.template_dir,
self.openstack_release,
self.configure_charm)]
return pebble_handlers
@property
def default_public_ingress_port(self):
return 8776
@property
def wsgi_container_name(self):
return CINDER_API_CONTAINER
def _do_bootstrap(self):
"""
Starts the appropriate services in the order they are needed.
If the service has not yet been bootstrapped, then this will
1. Create the database
"""
super()._do_bootstrap()
try:
container = self.unit.get_container(CINDER_SCHEDULER_CONTAINER)
logger.info("Syncing database...")
out = sunbeam_cprocess.check_output(
container,
[
'sudo', '-u', 'cinder',
'cinder-manage', '--config-dir',
'/etc/cinder', 'db', 'sync'],
service_name='keystone-db-sync',
timeout=180)
logging.debug(f'Output from database sync: \n{out}')
except sunbeam_cprocess.ContainerProcessError:
logger.exception('Failed to bootstrap')
self._state.bootstrapped = False
return
class CinderVictoriaOperatorCharm(CinderOperatorCharm):
openstack_release = 'victoria'
if __name__ == "__main__":
main(CinderOperatorCharm)
# Note: use_juju_for_storage=True required per
# https://github.com/canonical/operator/issues/506
main(CinderVictoriaOperatorCharm, use_juju_for_storage=True)

View File

@ -0,0 +1,26 @@
###############################################################################
# [ WARNING ]
# cinder configuration file maintained by Juju
# local changes may be overwritten.
###############################################################################
[DEFAULT]
rootwrap_config = /etc/cinder/rootwrap.conf
api_paste_confg = /etc/cinder/api-paste.ini
debug = {{ options.debug }}
transport_url = {{ amqp.transport_url }}
auth_strategy = keystone
{% include "parts/section-database" %}
[keystone_authtoken]
www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
auth_type = password
project_domain_name = {{ identity_service.service_domain_name }}
user_domain_name = {{ identity_service.service_domain_name }}
project_name = {{ identity_service.service_project_name }}
username = {{ identity_service.service_user_name }}
password = {{ identity_service.service_password }}
[oslo_concurrency]
lock_path = /var/lib/cinder/tmp

View File

@ -0,0 +1,7 @@
[database]
{% if cinder_db.database_host -%}
connection = {{ cinder_db.database_type }}://{{ cinder_db.database_user }}:{{ cinder_db.database_password }}@{{ cinder_db.database_host }}/{{ cinder_db.database }}{% if cinder_db.database_ssl_ca %}?ssl_ca={{ cinder_db.database_ssl_ca }}{% if cinder_db.database_ssl_cert %}&ssl_cert={{ cinder_db.database_ssl_cert }}&ssl_key={{ cinder_db.database_ssl_key }}{% endif %}{% endif %}
{% else -%}
connection = sqlite:////var/lib/cinder/cinder.db
{% endif -%}
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,6 @@
{% for section in sections -%}
[{{section}}]
{% for key, value in sections[section].items() -%}
{{ key }} = {{ value }}
{% endfor %}
{%- endfor %}

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,25 @@
Listen 8776
<VirtualHost *:8776>
WSGIDaemonProcess cinder processes=3 threads=1 user=cinder group=cinder \
display-name=%{GROUP}
WSGIProcessGroup cinder
WSGIScriptAlias / /usr/bin/cinder-wsgi
WSGIApplicationGroup %{GLOBAL}
WSGIPassAuthorization On
<IfVersion >= 2.4>
ErrorLogFormat "%{cu}t %M"
</IfVersion>
ErrorLog /var/log/apache2/cinder_error.log
CustomLog /var/log/apache2/cinder_access.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,17 @@
# This file is managed centrally. If you find the need to modify this as a
# one-off, please don't. Intead, consult #openstack-charms and ask about
# requirements management in charms via bot-control. Thank you.
charm-tools>=2.4.4
coverage>=3.6
mock>=1.2
flake8>=2.2.4,<=2.4.1
pyflakes==2.1.1
stestr>=2.2.0
requests>=2.18.4
psutil
# oslo.i18n dropped py35 support
oslo.i18n<4.0.0
git+https://github.com/openstack-charmers/zaza.git#egg=zaza
git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
pytz # workaround for 14.04 pip/tox
pyudev # for ceph-* charm unit tests (not mocked?)

View File

@ -56,11 +56,13 @@ class TestCharm(unittest.TestCase):
# Emit the PebbleReadyEvent carrying the httpbin container
self.harness.charm.on.httpbin_pebble_ready.emit(container)
# Get the plan now we've run PebbleReady
updated_plan = self.harness.get_container_pebble_plan("httpbin").to_dict()
updated_plan = self.harness.get_container_pebble_plan(
"httpbin").to_dict()
# Check we've got the plan we expected
self.assertEqual(expected_plan, updated_plan)
# Check the service was started
service = self.harness.model.unit.get_container("httpbin").get_service("httpbin")
service = self.harness.model.unit.get_container(
"httpbin").get_service("httpbin")
self.assertTrue(service.is_running())
# Ensure we set an ActiveStatus with no message
self.assertEqual(self.harness.model.unit.status, ActiveStatus())

134
charms/cinder-k8s/tox.ini Normal file
View File

@ -0,0 +1,134 @@
# Operator charm (with zaza): tox.ini
[tox]
envlist = pep8,py3
skipsdist = True
# NOTE: Avoid build/test env pollution by not enabling sitepackages.
sitepackages = False
# NOTE: Avoid false positives by not skipping missing interpreters.
skip_missing_interpreters = False
# NOTES:
# * We avoid the new dependency resolver by pinning pip < 20.3, see
# https://github.com/pypa/pip/issues/9187
# * Pinning dependencies requires tox >= 3.2.0, see
# https://tox.readthedocs.io/en/latest/config.html#conf-requires
# * It is also necessary to pin virtualenv as a newer virtualenv would still
# lead to fetching the latest pip in the func* tox targets, see
# https://stackoverflow.com/a/38133283
requires = pip < 20.3
virtualenv < 20.0
# NOTE: https://wiki.canonical.com/engineering/OpenStack/InstallLatestToxOnOsci
minversion = 3.2.0
[testenv]
setenv = VIRTUAL_ENV={envdir}
PYTHONHASHSEED=0
CHARM_DIR={envdir}
install_command =
pip install {opts} {packages}
commands = stestr run --slowest {posargs}
whitelist_externals =
git
add-to-archive.py
bash
charmcraft
passenv = HOME TERM CS_* OS_* TEST_*
deps = -r{toxinidir}/test-requirements.txt
[testenv:py35]
basepython = python3.5
# python3.5 is irrelevant on a focal+ charm.
commands = /bin/true
[testenv:py36]
basepython = python3.6
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
[testenv:py37]
basepython = python3.7
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
[testenv:py38]
basepython = python3.8
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
[testenv:py3]
basepython = python3
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
[testenv:pep8]
basepython = python3
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands = flake8 {posargs} src unit_tests tests
[testenv:cover]
# Technique based heavily upon
# https://github.com/openstack/nova/blob/master/tox.ini
basepython = python3
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
setenv =
{[testenv]setenv}
PYTHON=coverage run
commands =
coverage erase
stestr run --slowest {posargs}
coverage combine
coverage html -d cover
coverage xml -o cover/coverage.xml
coverage report
[coverage:run]
branch = True
concurrency = multiprocessing
parallel = True
source =
.
omit =
.tox/*
*/charmhelpers/*
unit_tests/*
[testenv:venv]
basepython = python3
commands = {posargs}
[testenv:build]
basepython = python3
deps = -r{toxinidir}/build-requirements.txt
commands =
charmcraft build
[testenv:func-noop]
basepython = python3
commands =
functest-run-suite --help
[testenv:func]
basepython = python3
commands =
functest-run-suite --keep-model
[testenv:func-smoke]
basepython = python3
commands =
functest-run-suite --keep-model --smoke
[testenv:func-dev]
basepython = python3
commands =
functest-run-suite --keep-model --dev
[testenv:func-target]
basepython = python3
commands =
functest-run-suite --keep-model --bundle {posargs}
[flake8]
# Ignore E902 because the unit_tests directory is missing in the built charm.
ignore = E402,E226,E902

View File

View File

@ -0,0 +1,63 @@
#!/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.
import unittest
import sys
sys.path.append('lib') # noqa
sys.path.append('src') # noqa
from ops.testing import Harness
import charm
class _CinderVictoriaOperatorCharm(charm.CinderVictoriaOperatorCharm):
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,
openstack_release, adapters):
self.render_calls.append(
(
containers,
container_configs,
template_dir,
openstack_release,
adapters))
def configure_charm(self, event):
super().configure_charm(event)
self._log_event(event)
class TestCinderOperatorCharm(unittest.TestCase):
def setUp(self):
self.harness = Harness(_CinderVictoriaOperatorCharm)
self.addCleanup(self.harness.cleanup)
self.harness.begin()
def test_pebble_ready_handler(self):
self.assertEqual(self.harness.charm.seen_events, [])
self.harness.container_pebble_ready('cinder-api')
self.assertEqual(self.harness.charm.seen_events, ['PebbleReadyEvent'])