Skip to content
Snippets Groups Projects
Commit ccfacbbb authored by sousaedu's avatar sousaedu
Browse files

Refactoring RO Charm to use Operator Framework


This refactoring work includes tests.

Note 1: old charm is in ro-k8s folder.
Note 2: relation-departed is currently not tested because there is
        no function to remove a relation in the Testing Harness.
        There is currently one issue open and the Charmcraft team
        should provide feedback soon.

Change-Id: Ia97de802aec2c4e10a1d2c86ba2515d3f17f55af
Signed-off-by: default avatarsousaedu <eduardo.sousa@canonical.com>
parent 83e7eb17
No related branches found
No related tags found
No related merge requests found
# Copyright 2020 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.
#
# For those usages not covered by the Apache License, Version 2.0 please
# contact: legal@canonical.com
#
# To get in touch with the maintainers, please contact:
# osm-charmers@lists.launchpad.net
##
venv
.vscode
build
ro.charm
.coverage
.stestr
cover
# Copyright 2020 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.
#
# For those usages not covered by the Apache License, Version 2.0 please
# contact: legal@canonical.com
#
# To get in touch with the maintainers, please contact:
# osm-charmers@lists.launchpad.net
##
---
extends: default
yaml-files:
- "*.yaml"
- "*.yml"
- ".yamllint"
ignore: |
.tox
build/
mod/
lib/
<!-- Copyright 2020 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.
For those usages not covered by the Apache License, Version 2.0 please
contact: legal@canonical.com
To get in touch with the maintainers, please contact:
osm-charmers@lists.launchpad.net -->
# RO operator Charm for Kubernetes
## Requirements
# Copyright 2020 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.
#
# For those usages not covered by the Apache License, Version 2.0 please
# contact: legal@canonical.com
#
# To get in touch with the maintainers, please contact:
# osm-charmers@lists.launchpad.net
##
options:
enable_ng_ro:
description: Enable NG-RO
type: boolean
default: true
database_commonkey:
description: Database COMMON KEY
type: string
default: osm
log_level:
description: "Log Level"
type: string
default: "INFO"
vim_database:
type: string
description: "The database name."
default: "mano_vim_db"
ro_database:
type: string
description: "The database name."
default: "mano_db"
openmano_tenant:
type: string
description: "Openmano Tenant"
default: "osm"
# Copyright 2020 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.
#
# For those usages not covered by the Apache License, Version 2.0 please
# contact: legal@canonical.com
#
# To get in touch with the maintainers, please contact:
# osm-charmers@lists.launchpad.net
##
name: ro
summary: OSM Resource Orchestrator (RO)
description: |
A CAAS charm to deploy OSM's Resource Orchestrator (RO).
series:
- kubernetes
tags:
- kubernetes
- osm
- ro
min-juju-version: 2.8.0
deployment:
type: stateless
service: cluster
resources:
image:
type: oci-image
description: OSM docker image for RO
upstream-source: "opensourcemano/ro:8"
provides:
ro:
interface: osm-ro
requires:
kafka:
interface: kafka
mongodb:
interface: mongodb
mysql:
interface: mysql
# Copyright 2020 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.
#
# For those usages not covered by the Apache License, Version 2.0 please
# contact: legal@canonical.com
#
# To get in touch with the maintainers, please contact:
# osm-charmers@lists.launchpad.net
##
ops
git+https://github.com/juju-solutions/resource-oci-image/@c5778285d332edf3d9a538f9d0c06154b7ec1b0b#egg=oci-image
#!/usr/bin/env python3
# Copyright 2020 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.
#
# For those usages not covered by the Apache License, Version 2.0 please
# contact: legal@canonical.com
#
# To get in touch with the maintainers, please contact:
# osm-charmers@lists.launchpad.net
##
import logging
from typing import Dict, List, NoReturn
from ops.charm import CharmBase
from ops.framework import EventBase, StoredState
from ops.main import main
from ops.model import ActiveStatus, Application, BlockedStatus, MaintenanceStatus, Unit
from oci_image import OCIImageResource, OCIImageResourceError
from pod_spec import make_pod_spec
logger = logging.getLogger(__name__)
RO_PORT = 9090
class RelationsMissing(Exception):
def __init__(self, missing_relations: List):
self.message = ""
if missing_relations and isinstance(missing_relations, list):
self.message += f'Waiting for {", ".join(missing_relations)} relation'
if "," in self.message:
self.message += "s"
class RelationDefinition:
def __init__(self, relation_name: str, keys: List, source_type):
if source_type != Application and source_type != Unit:
raise TypeError(
"source_type should be ops.model.Application or ops.model.Unit"
)
self.relation_name = relation_name
self.keys = keys
self.source_type = source_type
def check_missing_relation_data(
data: Dict,
expected_relations_data: List[RelationDefinition],
):
missing_relations = []
for relation_data in expected_relations_data:
if not all(
f"{relation_data.relation_name}_{k}" in data for k in relation_data.keys
):
missing_relations.append(relation_data.relation_name)
if missing_relations:
raise RelationsMissing(missing_relations)
def get_relation_data(
charm: CharmBase,
relation_data: RelationDefinition,
) -> Dict:
data = {}
relation = charm.model.get_relation(relation_data.relation_name)
if relation:
self_app_unit = (
charm.app if relation_data.source_type == Application else charm.unit
)
expected_type = relation_data.source_type
for app_unit in relation.data:
if app_unit != self_app_unit and isinstance(app_unit, expected_type):
if all(k in relation.data[app_unit] for k in relation_data.keys):
for k in relation_data.keys:
data[f"{relation_data.relation_name}_{k}"] = relation.data[
app_unit
].get(k)
break
return data
class RoCharm(CharmBase):
"""RO Charm."""
state = StoredState()
def __init__(self, *args) -> NoReturn:
"""RO Charm constructor."""
super().__init__(*args)
# Internal state initialization
self.state.set_default(pod_spec=None)
self.port = RO_PORT
self.image = OCIImageResource(self, "image")
# Registering regular events
self.framework.observe(self.on.start, self.configure_pod)
self.framework.observe(self.on.config_changed, self.configure_pod)
# Registering required relation events
self.framework.observe(self.on.kafka_relation_changed, self.configure_pod)
self.framework.observe(self.on.mongodb_relation_changed, self.configure_pod)
self.framework.observe(self.on.mysql_relation_changed, self.configure_pod)
# Registering required relation departed events
self.framework.observe(self.on.kafka_relation_departed, self.configure_pod)
self.framework.observe(self.on.mongodb_relation_departed, self.configure_pod)
self.framework.observe(self.on.mysql_relation_departed, self.configure_pod)
# Registering required relation broken events
self.framework.observe(self.on.kafka_relation_broken, self.configure_pod)
self.framework.observe(self.on.mongodb_relation_broken, self.configure_pod)
self.framework.observe(self.on.mysql_relation_broken, self.configure_pod)
# Registering provided relation events
self.framework.observe(self.on.ro_relation_joined, self._publish_ro_info)
def _publish_ro_info(self, event: EventBase) -> NoReturn:
"""Publishes RO information.
Args:
event (EventBase): RO relation event.
"""
if self.unit.is_leader():
rel_data = {
"host": self.model.app.name,
"port": str(RO_PORT),
}
for k, v in rel_data.items():
event.relation.data[self.app][k] = v
@property
def relations_requirements(self):
if self.model.config["enable_ng_ro"]:
return [
RelationDefinition("kafka", ["host", "port"], Unit),
RelationDefinition("mongodb", ["connection_string"], Unit),
]
else:
return [
RelationDefinition(
"mysql", ["host", "port", "user", "password", "root_password"], Unit
)
]
def get_relation_state(self):
relation_state = {}
for relation_requirements in self.relations_requirements:
data = get_relation_data(self, relation_requirements)
relation_state = {**relation_state, **data}
check_missing_relation_data(relation_state, self.relations_requirements)
return relation_state
def configure_pod(self, _=None) -> NoReturn:
"""Assemble the pod spec and apply it, if possible.
Args:
event (EventBase): Hook or Relation event that started the
function.
"""
if not self.unit.is_leader():
self.unit.status = ActiveStatus("ready")
return
relation_state = None
try:
relation_state = self.get_relation_state()
except RelationsMissing as exc:
logger.exception("Relation missing error")
self.unit.status = BlockedStatus(exc.message)
return
self.unit.status = MaintenanceStatus("Assembling pod spec")
# Fetch image information
try:
self.unit.status = MaintenanceStatus("Fetching image information")
image_info = self.image.fetch()
except OCIImageResourceError:
self.unit.status = BlockedStatus("Error fetching image information")
return
try:
pod_spec = make_pod_spec(
image_info,
self.model.config,
relation_state,
self.model.app.name,
self.port,
)
except ValueError as exc:
logger.exception("Config/Relation data validation error")
self.unit.status = BlockedStatus(str(exc))
return
if self.state.pod_spec != pod_spec:
self.model.pod.set_spec(pod_spec)
self.state.pod_spec = pod_spec
self.unit.status = ActiveStatus("ready")
if __name__ == "__main__":
main(RoCharm)
#!/usr/bin/env python3
# Copyright 2020 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.
#
# For those usages not covered by the Apache License, Version 2.0 please
# contact: legal@canonical.com
#
# To get in touch with the maintainers, please contact:
# osm-charmers@lists.launchpad.net
##
import logging
from typing import Any, Dict, List, NoReturn
logger = logging.getLogger(__name__)
def _validate_data(
config_data: Dict[str, Any], relation_data: Dict[str, Any]
) -> NoReturn:
"""Validates passed information.
Args:
config_data (Dict[str, Any]): configuration information.
relation_data (Dict[str, Any]): relation information
Raises:
ValueError: when config and/or relation data is not valid.
"""
config_validators = {
"enable_ng_ro": lambda value, _: isinstance(value, bool),
"database_commonkey": lambda value, values: (
isinstance(value, str) and len(value) > 0
)
if values.get("enable_ng_ro", True)
else True,
"log_level": lambda value, _: isinstance(value, str)
and value in ("INFO", "DEBUG"),
"vim_database": lambda value, values: (
isinstance(value, str) and len(value) > 0
)
if not values.get("enable_ng_ro", True)
else True,
"ro_database": lambda value, values: (isinstance(value, str) and len(value) > 0)
if not values.get("enable_ng_ro", True)
else True,
"openmano_tenant": lambda value, values: (
isinstance(value, str) and len(value) > 0
)
if not values.get("enable_ng_ro", True)
else True,
}
relation_validators = {
"kafka_host": lambda value, _: (isinstance(value, str) and len(value) > 0)
if config_data.get("enable_ng_ro", True)
else True,
"kafka_port": lambda value, _: (isinstance(value, int) and value > 0)
if config_data.get("enable_ng_ro", True)
else True,
"mongodb_connection_string": lambda value, _: (
isinstance(value, str) and value.startswith("mongodb://")
)
if config_data.get("enable_ng_ro", True)
else True,
"mysql_host": lambda value, _: (isinstance(value, str) and len(value) > 0)
if not config_data.get("enable_ng_ro", True)
else True,
"mysql_port": lambda value, _: (isinstance(value, int) and value > 0)
if not config_data.get("enable_ng_ro", True)
else True,
"mysql_user": lambda value, _: (isinstance(value, str) and len(value) > 0)
if not config_data.get("enable_ng_ro", True)
else True,
"mysql_password": lambda value, _: (isinstance(value, str) and len(value) > 0)
if not config_data.get("enable_ng_ro", True)
else True,
"mysql_root_password": lambda value, _: (
isinstance(value, str) and len(value) > 0
)
if not config_data.get("enable_ng_ro", True)
else True,
}
problems = []
for key, validator in config_validators.items():
valid = validator(config_data.get(key), config_data)
if not valid:
problems.append(key)
for key, validator in relation_validators.items():
valid = validator(relation_data.get(key), relation_data)
if not valid:
problems.append(key)
if len(problems) > 0:
raise ValueError("Errors found in: {}".format(", ".join(problems)))
def _make_pod_ports(port: int) -> List[Dict[str, Any]]:
"""Generate pod ports details.
Args:
port (int): port to expose.
Returns:
List[Dict[str, Any]]: pod port details.
"""
return [{"name": "ro", "containerPort": port, "protocol": "TCP"}]
def _make_pod_envconfig(
config: Dict[str, Any], relation_state: Dict[str, Any]
) -> Dict[str, Any]:
"""Generate pod environment configuration.
Args:
config (Dict[str, Any]): configuration information.
relation_state (Dict[str, Any]): relation state information.
Returns:
Dict[str, Any]: pod environment configuration.
"""
envconfig = {
# General configuration
"OSMRO_LOG_LEVEL": config["log_level"],
}
if config.get("enable_ng_ro", True):
# Kafka configuration
envconfig["OSMRO_MESSAGE_DRIVER"] = "kafka"
envconfig["OSMRO_MESSAGE_HOST"] = relation_state["kafka_host"]
envconfig["OSMRO_MESSAGE_PORT"] = relation_state["kafka_port"]
# MongoDB configuration
envconfig["OSMRO_DATABASE_DRIVER"] = "mongo"
envconfig["OSMRO_DATABASE_URI"] = relation_state["mongodb_connection_string"]
envconfig["OSMRO_DATABASE_COMMONKEY"] = config["database_commonkey"]
else:
envconfig["RO_DB_HOST"] = relation_state["mysql_host"]
envconfig["RO_DB_OVIM_HOST"] = relation_state["mysql_host"]
envconfig["RO_DB_PORT"] = relation_state["mysql_port"]
envconfig["RO_DB_OVIM_PORT"] = relation_state["mysql_port"]
envconfig["RO_DB_USER"] = relation_state["mysql_user"]
envconfig["RO_DB_OVIM_USER"] = relation_state["mysql_user"]
envconfig["RO_DB_PASSWORD"] = relation_state["mysql_password"]
envconfig["RO_DB_OVIM_PASSWORD"] = relation_state["mysql_password"]
envconfig["RO_DB_ROOT_PASSWORD"] = relation_state["mysql_root_password"]
envconfig["RO_DB_OVIM_ROOT_PASSWORD"] = relation_state["mysql_root_password"]
envconfig["RO_DB_NAME"] = config["ro_database"]
envconfig["RO_DB_OVIM_NAME"] = config["vim_database"]
envconfig["OPENMANO_TENANT"] = config["openmano_tenant"]
return envconfig
def _make_startup_probe() -> Dict[str, Any]:
"""Generate startup probe.
Returns:
Dict[str, Any]: startup probe.
"""
return {
"exec": {"command": ["/usr/bin/pgrep", "python3"]},
"initialDelaySeconds": 60,
"timeoutSeconds": 5,
}
def _make_readiness_probe(port: int) -> Dict[str, Any]:
"""Generate readiness probe.
Args:
port (int): service port.
Returns:
Dict[str, Any]: readiness probe.
"""
return {
"httpGet": {
"path": "/openmano/tenants",
"port": port,
},
"periodSeconds": 10,
"timeoutSeconds": 5,
"successThreshold": 1,
"failureThreshold": 3,
}
def _make_liveness_probe(port: int) -> Dict[str, Any]:
"""Generate liveness probe.
Args:
port (int): service port.
Returns:
Dict[str, Any]: liveness probe.
"""
return {
"httpGet": {
"path": "/openmano/tenants",
"port": port,
},
"initialDelaySeconds": 600,
"periodSeconds": 10,
"timeoutSeconds": 5,
"successThreshold": 1,
"failureThreshold": 3,
}
def make_pod_spec(
image_info: Dict[str, str],
config: Dict[str, Any],
relation_state: Dict[str, Any],
app_name: str = "ro",
port: int = 9090,
) -> Dict[str, Any]:
"""Generate the pod spec information.
Args:
image_info (Dict[str, str]): Object provided by
OCIImageResource("image").fetch().
config (Dict[str, Any]): Configuration information.
relation_state (Dict[str, Any]): Relation state information.
app_name (str, optional): Application name. Defaults to "ro".
port (int, optional): Port for the container. Defaults to 9090.
Returns:
Dict[str, Any]: Pod spec dictionary for the charm.
"""
if not image_info:
return None
_validate_data(config, relation_state)
ports = _make_pod_ports(port)
env_config = _make_pod_envconfig(config, relation_state)
startup_probe = _make_startup_probe()
readiness_probe = _make_readiness_probe(port)
liveness_probe = _make_liveness_probe(port)
return {
"version": 3,
"containers": [
{
"name": app_name,
"imageDetails": image_info,
"imagePullPolicy": "Always",
"ports": ports,
"envConfig": env_config,
"kubernetes": {
"startupProbe": startup_probe,
"readinessProbe": readiness_probe,
"livenessProbe": liveness_probe,
},
}
],
"kubernetesResources": {
"ingressResources": [],
},
}
#!/usr/bin/env python3
# Copyright 2020 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.
#
# For those usages not covered by the Apache License, Version 2.0 please
# contact: legal@canonical.com
#
# To get in touch with the maintainers, please contact:
# osm-charmers@lists.launchpad.net
##
"""Init mocking for unit tests."""
import sys
import mock
sys.path.append("src")
oci_image = mock.MagicMock()
sys.modules["oci_image"] = oci_image
#!/usr/bin/env python3
# Copyright 2020 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.
#
# For those usages not covered by the Apache License, Version 2.0 please
# contact: legal@canonical.com
#
# To get in touch with the maintainers, please contact:
# osm-charmers@lists.launchpad.net
##
from typing import NoReturn
import unittest
from ops.model import BlockedStatus
from ops.testing import Harness
from charm import RoCharm
class TestCharm(unittest.TestCase):
"""RO Charm unit tests."""
def setUp(self) -> NoReturn:
"""Test setup"""
self.harness = Harness(RoCharm)
self.harness.set_leader(is_leader=True)
self.harness.begin()
def test_on_start_without_relations_ng_ro(self) -> NoReturn:
"""Test installation without any relation."""
self.harness.charm.on.start.emit()
# Verifying status
self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
# Verifying status message
self.assertGreater(len(self.harness.charm.unit.status.message), 0)
self.assertTrue(
self.harness.charm.unit.status.message.startswith("Waiting for ")
)
self.assertIn("kafka", self.harness.charm.unit.status.message)
self.assertIn("mongodb", self.harness.charm.unit.status.message)
self.assertTrue(self.harness.charm.unit.status.message.endswith(" relations"))
def test_on_start_without_relations_no_ng_ro(self) -> NoReturn:
"""Test installation without any relation."""
self.harness.update_config({"enable_ng_ro": False})
self.harness.charm.on.start.emit()
# Verifying status
self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
# Verifying status message
self.assertGreater(len(self.harness.charm.unit.status.message), 0)
self.assertTrue(
self.harness.charm.unit.status.message.startswith("Waiting for ")
)
self.assertIn("mysql", self.harness.charm.unit.status.message)
self.assertTrue(self.harness.charm.unit.status.message.endswith(" relation"))
def test_on_start_with_relations_ng_ro(self) -> NoReturn:
"""Test deployment with NG-RO."""
expected_result = {
"version": 3,
"containers": [
{
"name": "ro",
"imageDetails": self.harness.charm.image.fetch(),
"imagePullPolicy": "Always",
"ports": [
{
"name": "ro",
"containerPort": 9090,
"protocol": "TCP",
}
],
"envConfig": {
"OSMRO_LOG_LEVEL": "INFO",
"OSMRO_MESSAGE_DRIVER": "kafka",
"OSMRO_MESSAGE_HOST": "kafka",
"OSMRO_MESSAGE_PORT": 9090,
"OSMRO_DATABASE_DRIVER": "mongo",
"OSMRO_DATABASE_URI": "mongodb://mongo",
"OSMRO_DATABASE_COMMONKEY": "osm",
},
"kubernetes": {
"startupProbe": {
"exec": {"command": ["/usr/bin/pgrep", "python3"]},
"initialDelaySeconds": 60,
"timeoutSeconds": 5,
},
"readinessProbe": {
"httpGet": {
"path": "/openmano/tenants",
"port": 9090,
},
"periodSeconds": 10,
"timeoutSeconds": 5,
"successThreshold": 1,
"failureThreshold": 3,
},
"livenessProbe": {
"httpGet": {
"path": "/openmano/tenants",
"port": 9090,
},
"initialDelaySeconds": 600,
"periodSeconds": 10,
"timeoutSeconds": 5,
"successThreshold": 1,
"failureThreshold": 3,
},
},
}
],
"kubernetesResources": {"ingressResources": []},
}
self.harness.charm.on.start.emit()
# Initializing the kafka relation
relation_id = self.harness.add_relation("kafka", "kafka")
self.harness.add_relation_unit(relation_id, "kafka/0")
self.harness.update_relation_data(
relation_id,
"kafka/0",
{
"host": "kafka",
"port": 9090,
},
)
# Initializing the mongodb relation
relation_id = self.harness.add_relation("mongodb", "mongodb")
self.harness.add_relation_unit(relation_id, "mongodb/0")
self.harness.update_relation_data(
relation_id,
"mongodb/0",
{
"connection_string": "mongodb://mongo",
},
)
# Verifying status
self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
pod_spec, _ = self.harness.get_pod_spec()
self.assertDictEqual(expected_result, pod_spec)
def test_on_start_with_relations_no_ng_ro(self) -> NoReturn:
"""Test deployment with old RO."""
self.harness.update_config({"enable_ng_ro": False})
expected_result = {
"version": 3,
"containers": [
{
"name": "ro",
"imageDetails": self.harness.charm.image.fetch(),
"imagePullPolicy": "Always",
"ports": [
{
"name": "ro",
"containerPort": 9090,
"protocol": "TCP",
}
],
"envConfig": {
"OSMRO_LOG_LEVEL": "INFO",
"RO_DB_HOST": "mysql",
"RO_DB_OVIM_HOST": "mysql",
"RO_DB_PORT": 3306,
"RO_DB_OVIM_PORT": 3306,
"RO_DB_USER": "mano",
"RO_DB_OVIM_USER": "mano",
"RO_DB_PASSWORD": "manopw",
"RO_DB_OVIM_PASSWORD": "manopw",
"RO_DB_ROOT_PASSWORD": "rootmanopw",
"RO_DB_OVIM_ROOT_PASSWORD": "rootmanopw",
"RO_DB_NAME": "mano_db",
"RO_DB_OVIM_NAME": "mano_vim_db",
"OPENMANO_TENANT": "osm",
},
"kubernetes": {
"startupProbe": {
"exec": {"command": ["/usr/bin/pgrep", "python3"]},
"initialDelaySeconds": 60,
"timeoutSeconds": 5,
},
"readinessProbe": {
"httpGet": {
"path": "/openmano/tenants",
"port": 9090,
},
"periodSeconds": 10,
"timeoutSeconds": 5,
"successThreshold": 1,
"failureThreshold": 3,
},
"livenessProbe": {
"httpGet": {
"path": "/openmano/tenants",
"port": 9090,
},
"initialDelaySeconds": 600,
"periodSeconds": 10,
"timeoutSeconds": 5,
"successThreshold": 1,
"failureThreshold": 3,
},
},
}
],
"kubernetesResources": {"ingressResources": []},
}
self.harness.charm.on.start.emit()
# Initializing the mysql relation
relation_id = self.harness.add_relation("mysql", "mysql")
self.harness.add_relation_unit(relation_id, "mysql/0")
self.harness.update_relation_data(
relation_id,
"mysql/0",
{
"host": "mysql",
"port": 3306,
"user": "mano",
"password": "manopw",
"root_password": "rootmanopw",
},
)
# Verifying status
self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
pod_spec, _ = self.harness.get_pod_spec()
self.assertDictEqual(expected_result, pod_spec)
def test_on_kafka_unit_relation_changed(self) -> NoReturn:
"""Test to see if kafka relation is updated."""
self.harness.charm.on.start.emit()
relation_id = self.harness.add_relation("kafka", "kafka")
self.harness.add_relation_unit(relation_id, "kafka/0")
self.harness.update_relation_data(
relation_id,
"kafka/0",
{
"host": "kafka",
"port": 9090,
},
)
# Verifying status
self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
# Verifying status message
self.assertGreater(len(self.harness.charm.unit.status.message), 0)
self.assertTrue(
self.harness.charm.unit.status.message.startswith("Waiting for ")
)
self.assertIn("mongodb", self.harness.charm.unit.status.message)
self.assertTrue(self.harness.charm.unit.status.message.endswith(" relation"))
def test_on_mongodb_unit_relation_changed(self) -> NoReturn:
"""Test to see if mongodb relation is updated."""
self.harness.charm.on.start.emit()
relation_id = self.harness.add_relation("mongodb", "mongodb")
self.harness.add_relation_unit(relation_id, "mongodb/0")
self.harness.update_relation_data(
relation_id,
"mongodb/0",
{
"connection_string": "mongodb://mongo",
},
)
# Verifying status
self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
# Verifying status message
self.assertGreater(len(self.harness.charm.unit.status.message), 0)
self.assertTrue(
self.harness.charm.unit.status.message.startswith("Waiting for ")
)
self.assertIn("kafka", self.harness.charm.unit.status.message)
self.assertTrue(self.harness.charm.unit.status.message.endswith(" relation"))
def test_on_mysql_unit_relation_changed(self) -> NoReturn:
"""Test to see if mysql relation is updated."""
self.harness.charm.on.start.emit()
relation_id = self.harness.add_relation("mysql", "mysql")
self.harness.add_relation_unit(relation_id, "mysql/0")
self.harness.update_relation_data(
relation_id,
"mysql/0",
{
"host": "mysql",
"port": 3306,
"user": "mano",
"password": "manopw",
"root_password": "rootmanopw",
},
)
# Verifying status
self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
# Verifying status message
self.assertGreater(len(self.harness.charm.unit.status.message), 0)
self.assertTrue(
self.harness.charm.unit.status.message.startswith("Waiting for ")
)
self.assertIn("kafka", self.harness.charm.unit.status.message)
self.assertIn("mongodb", self.harness.charm.unit.status.message)
self.assertTrue(self.harness.charm.unit.status.message.endswith(" relations"))
def test_publish_ro_info(self) -> NoReturn:
"""Test to see if ro relation is updated."""
expected_result = {
"host": "ro",
"port": "9090",
}
self.harness.charm.on.start.emit()
relation_id = self.harness.add_relation("ro", "lcm")
self.harness.add_relation_unit(relation_id, "lcm/0")
relation_data = self.harness.get_relation_data(relation_id, "ro")
self.assertDictEqual(expected_result, relation_data)
if __name__ == "__main__":
unittest.main()
#!/usr/bin/env python3
# Copyright 2020 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.
#
# For those usages not covered by the Apache License, Version 2.0 please
# contact: legal@canonical.com
#
# To get in touch with the maintainers, please contact:
# osm-charmers@lists.launchpad.net
##
from typing import NoReturn
import unittest
import pod_spec
class TestPodSpec(unittest.TestCase):
"""Pod spec unit tests."""
def test_make_pod_ports(self) -> NoReturn:
"""Testing make pod ports."""
port = 9090
expected_result = [
{
"name": "ro",
"containerPort": port,
"protocol": "TCP",
}
]
pod_ports = pod_spec._make_pod_ports(port)
self.assertListEqual(expected_result, pod_ports)
def test_make_pod_envconfig_ng_ro(self) -> NoReturn:
"""Teting make pod envconfig."""
config = {
"enable_ng_ro": True,
"database_commonkey": "osm",
"log_level": "INFO",
}
relation_state = {
"kafka_host": "kafka",
"kafka_port": 9090,
"mongodb_connection_string": "mongodb://mongo",
}
expected_result = {
"OSMRO_LOG_LEVEL": config["log_level"],
"OSMRO_MESSAGE_DRIVER": "kafka",
"OSMRO_MESSAGE_HOST": relation_state["kafka_host"],
"OSMRO_MESSAGE_PORT": relation_state["kafka_port"],
"OSMRO_DATABASE_DRIVER": "mongo",
"OSMRO_DATABASE_URI": relation_state["mongodb_connection_string"],
"OSMRO_DATABASE_COMMONKEY": config["database_commonkey"],
}
pod_envconfig = pod_spec._make_pod_envconfig(config, relation_state)
self.assertDictEqual(expected_result, pod_envconfig)
def test_make_pod_envconfig_no_ng_ro(self) -> NoReturn:
"""Teting make pod envconfig."""
config = {
"log_level": "INFO",
"enable_ng_ro": False,
"vim_database": "mano_vim_db",
"ro_database": "mano_db",
"openmano_tenant": "osm",
}
relation_state = {
"mysql_host": "mysql",
"mysql_port": 3306,
"mysql_user": "mano",
"mysql_password": "manopw",
"mysql_root_password": "rootmanopw",
}
expected_result = {
"OSMRO_LOG_LEVEL": config["log_level"],
"RO_DB_HOST": relation_state["mysql_host"],
"RO_DB_OVIM_HOST": relation_state["mysql_host"],
"RO_DB_PORT": relation_state["mysql_port"],
"RO_DB_OVIM_PORT": relation_state["mysql_port"],
"RO_DB_USER": relation_state["mysql_user"],
"RO_DB_OVIM_USER": relation_state["mysql_user"],
"RO_DB_PASSWORD": relation_state["mysql_password"],
"RO_DB_OVIM_PASSWORD": relation_state["mysql_password"],
"RO_DB_ROOT_PASSWORD": relation_state["mysql_root_password"],
"RO_DB_OVIM_ROOT_PASSWORD": relation_state["mysql_root_password"],
"RO_DB_NAME": config["ro_database"],
"RO_DB_OVIM_NAME": config["vim_database"],
"OPENMANO_TENANT": config["openmano_tenant"],
}
pod_envconfig = pod_spec._make_pod_envconfig(config, relation_state)
self.assertDictEqual(expected_result, pod_envconfig)
def test_make_startup_probe(self) -> NoReturn:
"""Testing make startup probe."""
expected_result = {
"exec": {"command": ["/usr/bin/pgrep", "python3"]},
"initialDelaySeconds": 60,
"timeoutSeconds": 5,
}
startup_probe = pod_spec._make_startup_probe()
self.assertDictEqual(expected_result, startup_probe)
def test_make_readiness_probe(self) -> NoReturn:
"""Testing make readiness probe."""
port = 9090
expected_result = {
"httpGet": {
"path": "/openmano/tenants",
"port": port,
},
"periodSeconds": 10,
"timeoutSeconds": 5,
"successThreshold": 1,
"failureThreshold": 3,
}
readiness_probe = pod_spec._make_readiness_probe(port)
self.assertDictEqual(expected_result, readiness_probe)
def test_make_liveness_probe(self) -> NoReturn:
"""Testing make liveness probe."""
port = 9090
expected_result = {
"httpGet": {
"path": "/openmano/tenants",
"port": port,
},
"initialDelaySeconds": 600,
"periodSeconds": 10,
"timeoutSeconds": 5,
"successThreshold": 1,
"failureThreshold": 3,
}
liveness_probe = pod_spec._make_liveness_probe(port)
self.assertDictEqual(expected_result, liveness_probe)
def test_make_pod_spec_ng_ro(self) -> NoReturn:
"""Testing make pod spec."""
image_info = {"upstream-source": "opensourcemano/ro:8"}
config = {
"database_commonkey": "osm",
"log_level": "INFO",
"enable_ng_ro": True,
}
relation_state = {
"kafka_host": "kafka",
"kafka_port": 9090,
"mongodb_connection_string": "mongodb://mongo",
}
app_name = "ro"
port = 9090
expected_result = {
"version": 3,
"containers": [
{
"name": app_name,
"imageDetails": image_info,
"imagePullPolicy": "Always",
"ports": [
{
"name": app_name,
"containerPort": port,
"protocol": "TCP",
}
],
"envConfig": {
"OSMRO_LOG_LEVEL": config["log_level"],
"OSMRO_MESSAGE_DRIVER": "kafka",
"OSMRO_MESSAGE_HOST": relation_state["kafka_host"],
"OSMRO_MESSAGE_PORT": relation_state["kafka_port"],
"OSMRO_DATABASE_DRIVER": "mongo",
"OSMRO_DATABASE_URI": relation_state[
"mongodb_connection_string"
],
"OSMRO_DATABASE_COMMONKEY": config["database_commonkey"],
},
"kubernetes": {
"startupProbe": {
"exec": {"command": ["/usr/bin/pgrep", "python3"]},
"initialDelaySeconds": 60,
"timeoutSeconds": 5,
},
"readinessProbe": {
"httpGet": {
"path": "/openmano/tenants",
"port": port,
},
"periodSeconds": 10,
"timeoutSeconds": 5,
"successThreshold": 1,
"failureThreshold": 3,
},
"livenessProbe": {
"httpGet": {
"path": "/openmano/tenants",
"port": port,
},
"initialDelaySeconds": 600,
"periodSeconds": 10,
"timeoutSeconds": 5,
"successThreshold": 1,
"failureThreshold": 3,
},
},
}
],
"kubernetesResources": {"ingressResources": []},
}
spec = pod_spec.make_pod_spec(
image_info, config, relation_state, app_name, port
)
self.assertDictEqual(expected_result, spec)
def test_make_pod_spec_no_ng_ro(self) -> NoReturn:
"""Testing make pod spec."""
image_info = {"upstream-source": "opensourcemano/ro:8"}
config = {
"log_level": "INFO",
"enable_ng_ro": False,
"vim_database": "mano_vim_db",
"ro_database": "mano_db",
"openmano_tenant": "osm",
}
relation_state = {
"mysql_host": "mysql",
"mysql_port": 3306,
"mysql_user": "mano",
"mysql_password": "manopw",
"mysql_root_password": "rootmanopw",
}
app_name = "ro"
port = 9090
expected_result = {
"version": 3,
"containers": [
{
"name": app_name,
"imageDetails": image_info,
"imagePullPolicy": "Always",
"ports": [
{
"name": app_name,
"containerPort": port,
"protocol": "TCP",
}
],
"envConfig": {
"OSMRO_LOG_LEVEL": config["log_level"],
"RO_DB_HOST": relation_state["mysql_host"],
"RO_DB_OVIM_HOST": relation_state["mysql_host"],
"RO_DB_PORT": relation_state["mysql_port"],
"RO_DB_OVIM_PORT": relation_state["mysql_port"],
"RO_DB_USER": relation_state["mysql_user"],
"RO_DB_OVIM_USER": relation_state["mysql_user"],
"RO_DB_PASSWORD": relation_state["mysql_password"],
"RO_DB_OVIM_PASSWORD": relation_state["mysql_password"],
"RO_DB_ROOT_PASSWORD": relation_state["mysql_root_password"],
"RO_DB_OVIM_ROOT_PASSWORD": relation_state[
"mysql_root_password"
],
"RO_DB_NAME": config["ro_database"],
"RO_DB_OVIM_NAME": config["vim_database"],
"OPENMANO_TENANT": config["openmano_tenant"],
},
"kubernetes": {
"startupProbe": {
"exec": {"command": ["/usr/bin/pgrep", "python3"]},
"initialDelaySeconds": 60,
"timeoutSeconds": 5,
},
"readinessProbe": {
"httpGet": {
"path": "/openmano/tenants",
"port": port,
},
"periodSeconds": 10,
"timeoutSeconds": 5,
"successThreshold": 1,
"failureThreshold": 3,
},
"livenessProbe": {
"httpGet": {
"path": "/openmano/tenants",
"port": port,
},
"initialDelaySeconds": 600,
"periodSeconds": 10,
"timeoutSeconds": 5,
"successThreshold": 1,
"failureThreshold": 3,
},
},
}
],
"kubernetesResources": {"ingressResources": []},
}
spec = pod_spec.make_pod_spec(
image_info, config, relation_state, app_name, port
)
self.assertDictEqual(expected_result, spec)
def test_make_pod_spec_without_image_info(self) -> NoReturn:
"""Testing make pod spec without image_info."""
image_info = None
config = {
"enable_ng_ro": True,
"database_commonkey": "osm",
"log_level": "INFO",
}
relation_state = {
"kafka_host": "kafka",
"kafka_port": 9090,
"mongodb_connection_string": "mongodb://mongo",
}
app_name = "ro"
port = 9090
spec = pod_spec.make_pod_spec(
image_info, config, relation_state, app_name, port
)
self.assertIsNone(spec)
def test_make_pod_spec_without_config(self) -> NoReturn:
"""Testing make pod spec without config."""
image_info = {"upstream-source": "opensourcemano/ro:8"}
config = {}
relation_state = {
"kafka_host": "kafka",
"kafka_port": 9090,
"mongodb_connection_string": "mongodb://mongo",
}
app_name = "ro"
port = 9090
with self.assertRaises(ValueError):
pod_spec.make_pod_spec(image_info, config, relation_state, app_name, port)
def test_make_pod_spec_without_relation_state(self) -> NoReturn:
"""Testing make pod spec without relation_state."""
image_info = {"upstream-source": "opensourcemano/ro:8"}
config = {
"enable_ng_ro": True,
"database_commonkey": "osm",
"log_level": "INFO",
}
relation_state = {}
app_name = "ro"
port = 9090
with self.assertRaises(ValueError):
pod_spec.make_pod_spec(image_info, config, relation_state, app_name, port)
if __name__ == "__main__":
unittest.main()
# Copyright 2020 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.
#
# For those usages not covered by the Apache License, Version 2.0 please
# contact: legal@canonical.com
#
# To get in touch with the maintainers, please contact:
# osm-charmers@lists.launchpad.net
##
[tox]
skipsdist = True
envlist = unit, lint
sitepackages = False
skip_missing_interpreters = False
[testenv]
basepython = python3
setenv =
PYTHONHASHSEED=0
PYTHONPATH = {toxinidir}/src
CHARM_NAME = ro
[testenv:build]
passenv=HTTP_PROXY HTTPS_PROXY NO_PROXY
whitelist_externals =
charmcraft
rm
unzip
commands =
rm -rf release ro.charm
charmcraft build
unzip ro.charm -d release
[testenv:unit]
commands =
coverage erase
stestr run --slowest --test-path=./tests --top-dir=./
coverage combine
coverage html -d cover
coverage xml -o cover/coverage.xml
coverage report
deps =
coverage
stestr
mock
ops
setenv =
{[testenv]setenv}
PYTHON=coverage run
[testenv:lint]
deps =
black
yamllint
flake8
commands =
black --check --diff . --exclude "build/|.tox/|mod/|lib/"
yamllint .
flake8 . --max-line-length=100 --ignore="E501,W503,W504,F722" --exclude "build/ .tox/ mod/ lib/"
[coverage:run]
branch = True
concurrency = multiprocessing
parallel = True
source =
.
omit =
.tox/*
tests/*
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment