Switch to using aso
This commit is contained in:
parent
15e6d6536d
commit
3b13d29aed
4
charms/cinder-k8s/.gitignore
vendored
4
charms/cinder-k8s/.gitignore
vendored
@ -1,7 +1,9 @@
|
||||
venv/
|
||||
build/
|
||||
.stestr/
|
||||
*.charm
|
||||
|
||||
.tox
|
||||
.coverage
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
lib/charms/sunbeam_rabbitmq_operator/
|
||||
|
3
charms/cinder-k8s/.stestr.conf
Normal file
3
charms/cinder-k8s/.stestr.conf
Normal file
@ -0,0 +1,3 @@
|
||||
[DEFAULT]
|
||||
test_path=./unit_tests
|
||||
top_dir=./
|
2
charms/cinder-k8s/build-requirements.txt
Normal file
2
charms/cinder-k8s/build-requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
git+https://github.com/canonical/charmcraft.git@0.10.2#egg=charmcraft
|
||||
|
@ -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
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
26
charms/cinder-k8s/src/templates/cinder.conf.j2
Normal file
26
charms/cinder-k8s/src/templates/cinder.conf.j2
Normal 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
|
7
charms/cinder-k8s/src/templates/parts/section-database
Normal file
7
charms/cinder-k8s/src/templates/parts/section-database
Normal 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
|
10
charms/cinder-k8s/src/templates/parts/section-federation
Normal file
10
charms/cinder-k8s/src/templates/parts/section-federation
Normal 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 -%}
|
6
charms/cinder-k8s/src/templates/parts/section-middleware
Normal file
6
charms/cinder-k8s/src/templates/parts/section-middleware
Normal file
@ -0,0 +1,6 @@
|
||||
{% for section in sections -%}
|
||||
[{{section}}]
|
||||
{% for key, value in sections[section].items() -%}
|
||||
{{ key }} = {{ value }}
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
15
charms/cinder-k8s/src/templates/parts/section-signing
Normal file
15
charms/cinder-k8s/src/templates/parts/section-signing
Normal 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 -%}
|
25
charms/cinder-k8s/src/templates/wsgi-cinder.conf.j2
Normal file
25
charms/cinder-k8s/src/templates/wsgi-cinder.conf.j2
Normal 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>
|
||||
|
17
charms/cinder-k8s/test-requirements.txt
Normal file
17
charms/cinder-k8s/test-requirements.txt
Normal 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?)
|
@ -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
134
charms/cinder-k8s/tox.ini
Normal 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
|
0
charms/cinder-k8s/unit_tests/__init__.py
Normal file
0
charms/cinder-k8s/unit_tests/__init__.py
Normal file
63
charms/cinder-k8s/unit_tests/test_cinder_charm.py
Normal file
63
charms/cinder-k8s/unit_tests/test_cinder_charm.py
Normal 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'])
|
Loading…
x
Reference in New Issue
Block a user