Add Kafka and Zookeeper charms in operator framework
Change-Id: I15645825ab8ff927ad0f72bbfd53ea71343b2be4
Signed-off-by: David Garcia <david.garcia@canonical.com>
diff --git a/installers/charm/zookeeper/.gitignore b/installers/charm/zookeeper/.gitignore
new file mode 100644
index 0000000..a85ce6d
--- /dev/null
+++ b/installers/charm/zookeeper/.gitignore
@@ -0,0 +1,28 @@
+# 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.
+#
+# 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
+*.charm
+.coverage
+coverage.xml
+.stestr
+cover
\ No newline at end of file
diff --git a/installers/charm/zookeeper/.jujuignore b/installers/charm/zookeeper/.jujuignore
new file mode 100644
index 0000000..3738c1c
--- /dev/null
+++ b/installers/charm/zookeeper/.jujuignore
@@ -0,0 +1,32 @@
+# 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.
+#
+# 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
+*.charm
+.coverage
+coverage.xml
+.gitignore
+.stestr
+cover
+tests/
+requirements*
+tox.ini
diff --git a/installers/charm/zookeeper/.yamllint.yaml b/installers/charm/zookeeper/.yamllint.yaml
new file mode 100644
index 0000000..5244c94
--- /dev/null
+++ b/installers/charm/zookeeper/.yamllint.yaml
@@ -0,0 +1,32 @@
+# 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.
+#
+# 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
+ cover/
+ venv
diff --git a/installers/charm/zookeeper/README.md b/installers/charm/zookeeper/README.md
new file mode 100644
index 0000000..bc6aec7
--- /dev/null
+++ b/installers/charm/zookeeper/README.md
@@ -0,0 +1,22 @@
+<!-- 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 -->
+
+# Zookeeper operator Charm for Kubernetes
+
diff --git a/installers/charm/zookeeper/charmcraft.yaml b/installers/charm/zookeeper/charmcraft.yaml
new file mode 100644
index 0000000..0a285a9
--- /dev/null
+++ b/installers/charm/zookeeper/charmcraft.yaml
@@ -0,0 +1,37 @@
+# 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.
+#
+# 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
+##
+
+type: charm
+bases:
+ - build-on:
+ - name: ubuntu
+ channel: "20.04"
+ architectures: ["amd64"]
+ run-on:
+ - name: ubuntu
+ channel: "20.04"
+ architectures:
+ - amd64
+ - aarch64
+ - arm64
+parts:
+ charm:
+ build-packages: [git]
diff --git a/installers/charm/zookeeper/config.yaml b/installers/charm/zookeeper/config.yaml
new file mode 100644
index 0000000..d9b89a4
--- /dev/null
+++ b/installers/charm/zookeeper/config.yaml
@@ -0,0 +1,89 @@
+# 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.
+#
+# 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:
+ log_level:
+ description: |
+ Log level
+ type: string
+ default: INFO
+ image_pull_policy:
+ description: |
+ ImagePullPolicy configuration for the pod.
+ Possible values: always, ifnotpresent, never
+ type: string
+ default: always
+ min_session_timeout:
+ description: Min session timeout
+ type: int
+ default: 4000
+ max_session_timeout:
+ description: Max session timeout
+ type: int
+ default: 40000
+ purge_interval:
+ description: |
+ The time interval in hours for which the purge task has to be triggered.
+ Set to a positive integer (1 and above) to enable the auto purging.
+ type: int
+ default: 12
+ snap_retain_count:
+ description: |
+ When enabled, ZooKeeper auto purge feature retains the
+ autopurge.snapRetainCount most recent snapshots and
+ the corresponding transaction logs in the dataDir and
+ dataLogDir respectively and deletes the rest.
+ Defaults to 3. Minimum value is 3.
+ type: int
+ default: 3
+ max_client_cnxns:
+ description: |
+ Limits the number of concurrent connections (at the socket level)
+ that a single client, identified by IP address, may make to a single
+ member of the ZooKeeper ensemble.
+ type: int
+ default: 60
+ heap:
+ description: Heap memory in Mega-bytes
+ type: int
+ default: 512
+ sync_limit:
+ description: |
+ Amount of time, in ticks (see tickTime), to allow followers to sync
+ with ZooKeeper.
+ If followers fall too far behind a leader, they will be dropped.
+ type: int
+ default: 5
+ init_limit:
+ description: |
+ Amount of time, in ticks (see tickTime), to allow followers to connect
+ and sync to a leader. Increased this value as needed,
+ if the amount of data managed by ZooKeeper is large.
+ type: int
+ default: 5
+ tick_time:
+ description: |
+ The length of a single tick, which is the basic time unit used
+ by ZooKeeper, as measured in milliseconds. It is used to regulate
+ heartbeats, and timeouts.
+ For example, the minimum session timeout will be two ticks.
+ type: int
+ default: 2000
diff --git a/installers/charm/zookeeper/metadata.yaml b/installers/charm/zookeeper/metadata.yaml
new file mode 100644
index 0000000..5a8bfb4
--- /dev/null
+++ b/installers/charm/zookeeper/metadata.yaml
@@ -0,0 +1,51 @@
+# 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: zookeeper
+summary: "Zookeeper charm for Kubernetes."
+description: |
+ A CAAS charm to deploy zookeeper.
+series:
+ - kubernetes
+tags:
+ - kubernetes
+ - osm
+ - zookeeper
+min-juju-version: 2.8.0
+resources:
+ image:
+ type: oci-image
+ description: OSM docker image for zookeeper
+ upstream-source: |
+ "rocks.canonical.com:443/k8s.gcr.io/kubernetes-zookeeper:1.0-3.4.10"
+provides:
+ zookeeper:
+ interface: zookeeper
+peers:
+ cluster:
+ interface: zookeeper-cluster
+storage:
+ database:
+ type: filesystem
+ location: /var/lib/zookeeper
+deployment:
+ type: stateful
+ service: cluster
diff --git a/installers/charm/zookeeper/requirements-test.txt b/installers/charm/zookeeper/requirements-test.txt
new file mode 100644
index 0000000..316f6d2
--- /dev/null
+++ b/installers/charm/zookeeper/requirements-test.txt
@@ -0,0 +1,21 @@
+# 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.
+#
+# 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
+
+mock==4.0.3
diff --git a/installers/charm/zookeeper/requirements.txt b/installers/charm/zookeeper/requirements.txt
new file mode 100644
index 0000000..1a8928c
--- /dev/null
+++ b/installers/charm/zookeeper/requirements.txt
@@ -0,0 +1,22 @@
+# 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.
+#
+# 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
+##
+
+git+https://github.com/charmed-osm/ops-lib-charmed-osm/@master
\ No newline at end of file
diff --git a/installers/charm/zookeeper/src/charm.py b/installers/charm/zookeeper/src/charm.py
new file mode 100755
index 0000000..6e4588c
--- /dev/null
+++ b/installers/charm/zookeeper/src/charm.py
@@ -0,0 +1,181 @@
+#!/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.
+#
+# 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
+##
+
+# pylint: disable=E0213
+
+import logging
+from typing import NoReturn
+
+
+from ops.framework import EventBase
+from ops.main import main
+from opslib.osm.charm import CharmedOsmBase
+from opslib.osm.interfaces.zookeeper import ZookeeperCluster, ZookeeperServer
+from opslib.osm.pod import ContainerV3Builder, PodSpecV3Builder
+from opslib.osm.validator import ModelValidator, validator
+
+logger = logging.getLogger(__name__)
+
+CLIENT_PORT = 2181
+SERVER_PORT = 2888
+LEADER_ELECTION_PORT = 3888
+
+
+class ConfigModel(ModelValidator):
+ log_level: str
+ image_pull_policy: str
+ min_session_timeout: int
+ max_session_timeout: int
+ purge_interval: int
+ snap_retain_count: int
+ max_client_cnxns: int
+ heap: int
+ sync_limit: int
+ init_limit: int
+ tick_time: int
+
+ @validator("log_level")
+ def validate_log_level(cls, v):
+ if v not in {"INFO", "DEBUG"}:
+ raise ValueError("value must be INFO or DEBUG")
+ return v
+
+ @validator("image_pull_policy")
+ def validate_image_pull_policy(cls, v):
+ values = {
+ "always": "Always",
+ "ifnotpresent": "IfNotPresent",
+ "never": "Never",
+ }
+ v = v.lower()
+ if v not in values.keys():
+ raise ValueError("value must be always, ifnotpresent or never")
+ return values[v]
+
+
+class ZookeeperCharm(CharmedOsmBase):
+ """Zookeeper Charm."""
+
+ def __init__(self, *args) -> NoReturn:
+ """Zookeeper Charm constructor."""
+ super().__init__(*args, oci_image="image")
+ # Initialize Zookeeper cluster relation
+ self.zookeeper_cluster = ZookeeperCluster(self, "cluster", CLIENT_PORT)
+ self.framework.observe(self.on["cluster"].relation_changed, self._setup_cluster)
+ # Initialize Zookeeper relation
+ self.zookeeper_server = ZookeeperServer(self, "zookeeper")
+ self.framework.observe(self.on["zookeeper"].relation_joined, self._publish_info)
+
+ @property
+ def num_units(self):
+ return self.zookeeper_cluster.num_units
+
+ @property
+ def zookeeper_uri(self):
+ return self.zookeeper_cluster.zookeeper_uri
+
+ def _setup_cluster(self, event: EventBase):
+ """Publishes Zookeeper information and reconfigures the pod.
+
+ Args:
+ event (EventBase): Zookeeper Cluster relation event.
+ """
+ self._publish_zookeeper_info(event)
+ self.configure_pod()
+
+ def _publish_info(self, event: EventBase):
+ """Publishes Zookeeper information.
+
+ Args:
+ event (EventBase): Zookeeper relation event.
+ """
+ if self.unit.is_leader():
+ zk_uri = self.zookeeper_uri
+ if zk_uri:
+ self.zookeeper_server.publish_info(zk_uri)
+ else:
+ event.defer()
+
+ def build_pod_spec(self, image_info):
+ # Validate config
+ config = ConfigModel(**dict(self.config))
+
+ # Create Builder for the PodSpec
+ pod_spec_builder = PodSpecV3Builder()
+
+ # Build Container
+ container_builder = ContainerV3Builder(
+ self.app.name, image_info, config.image_pull_policy
+ )
+
+ container_builder.add_port(name="client", port=CLIENT_PORT)
+ container_builder.add_port(name="server", port=SERVER_PORT)
+ container_builder.add_port(name="leader-election", port=LEADER_ELECTION_PORT)
+ container_builder.add_tcpsocket_readiness_probe(
+ CLIENT_PORT,
+ initial_delay_seconds=10,
+ timeout_seconds=5,
+ failure_threshold=6,
+ success_threshold=1,
+ )
+ container_builder.add_tcpsocket_liveness_probe(
+ CLIENT_PORT, initial_delay_seconds=20
+ )
+ container_builder.add_command(
+ [
+ "sh",
+ "-c",
+ " ".join(
+ [
+ "start-zookeeper",
+ f"--servers={self.num_units}",
+ "--data_dir=/var/lib/zookeeper/data",
+ "--data_log_dir=/var/lib/zookeeper/data/log",
+ "--conf_dir=/opt/zookeeper/conf",
+ f"--client_port={CLIENT_PORT}",
+ f"--election_port={LEADER_ELECTION_PORT}",
+ f"--server_port={SERVER_PORT}",
+ f"--tick_time={config.tick_time}",
+ f"--init_limit={config.init_limit}",
+ f"--sync_limit={config.sync_limit}",
+ f"--heap={config.heap}M",
+ f"--max_client_cnxns={config.max_client_cnxns}",
+ f"--snap_retain_count={config.snap_retain_count}",
+ f"--purge_interval={config.purge_interval}",
+ f"--max_session_timeout={config.max_session_timeout}",
+ f"--min_session_timeout={config.min_session_timeout}",
+ f"--log_level={config.log_level}",
+ ]
+ ),
+ ]
+ )
+
+ container = container_builder.build()
+
+ # Add container to pod spec
+ pod_spec_builder.add_container(container)
+
+ return pod_spec_builder.build()
+
+
+if __name__ == "__main__":
+ main(ZookeeperCharm)
diff --git a/installers/charm/zookeeper/tests/__init__.py b/installers/charm/zookeeper/tests/__init__.py
new file mode 100644
index 0000000..446d5ce
--- /dev/null
+++ b/installers/charm/zookeeper/tests/__init__.py
@@ -0,0 +1,40 @@
+#!/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
+
+
+class OCIImageResourceErrorMock(Exception):
+ pass
+
+
+sys.path.append("src")
+
+oci_image = mock.MagicMock()
+oci_image.OCIImageResourceError = OCIImageResourceErrorMock
+sys.modules["oci_image"] = oci_image
+sys.modules["oci_image"].OCIImageResource().fetch.return_value = {}
diff --git a/installers/charm/zookeeper/tests/test_charm.py b/installers/charm/zookeeper/tests/test_charm.py
new file mode 100644
index 0000000..27d3401
--- /dev/null
+++ b/installers/charm/zookeeper/tests/test_charm.py
@@ -0,0 +1,94 @@
+#!/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 unittest.mock import patch, PropertyMock
+
+from charm import ZookeeperCharm
+from ops.model import ActiveStatus, BlockedStatus
+from ops.testing import Harness
+
+
+class TestCharm(unittest.TestCase):
+ """Zookeeper Charm unit tests."""
+
+ def setUp(
+ self,
+ ) -> NoReturn:
+ """Test setup"""
+ self.harness = Harness(ZookeeperCharm)
+ self.harness.set_leader(is_leader=True)
+ self.config = {"log_level": "INFO", "image_pull_pulicy": "always"}
+ self.harness.begin()
+
+ def test_config_invalid_log_level(self) -> NoReturn:
+ """Test invalid log_level config."""
+ self.config.update({"log_level": "invalid log level"})
+ self.harness.update_config(self.config)
+ self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
+
+ def test_config_invalid_image_pull_pulicy(self) -> NoReturn:
+ """Test invalid image_pull_pulicy config."""
+ self.config.update({"image_pull_policy": "invalid image_pull_policy"})
+ self.harness.update_config(self.config)
+ self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
+
+ @patch("charm.ZookeeperCharm.num_units", new_callable=PropertyMock)
+ def test_config_changed_no_relations(self, mock_num_units) -> NoReturn:
+ """Test config changed without relations."""
+ mock_num_units.return_value = 1
+ self.harness.charm.on.config_changed.emit()
+ self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
+
+ @patch("charm.ZookeeperCharm.num_units", new_callable=PropertyMock)
+ def test_config_changed_non_leader(self, mock_num_units) -> NoReturn:
+ """Test config changed without relations (non-leader)."""
+ mock_num_units.return_value = 1
+ self.harness.set_leader(is_leader=False)
+ self.harness.charm.on.config_changed.emit()
+
+ # Assertions
+ self.assertIsInstance(self.harness.charm.unit.status, ActiveStatus)
+
+ @patch("charm.ZookeeperCharm.num_units", new_callable=PropertyMock)
+ @patch("charm.ZookeeperCharm.zookeeper_uri", new_callable=PropertyMock)
+ def test_with_relations_zookeeper(
+ self, mock_zookeeper_uri, mock_num_units
+ ) -> NoReturn:
+ "Test with relations (zookeeper)"
+ mock_num_units.return_value = 1
+ mock_zookeeper_uri.return_value = "zk-uri"
+
+ # Initializing the zookeeper relation
+ zookeeper_relation_id = self.harness.add_relation("zookeeper", "kafka")
+ self.harness.add_relation_unit(zookeeper_relation_id, "kafka/0")
+ # self.harness.update_relation_data(
+ # zookeeper_relation_id, "kafka/0", {"host": "zookeeper", "port": 9092}
+ # )
+
+ # Verifying status
+ self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/installers/charm/zookeeper/tox.ini b/installers/charm/zookeeper/tox.ini
new file mode 100644
index 0000000..c341c8e
--- /dev/null
+++ b/installers/charm/zookeeper/tox.ini
@@ -0,0 +1,126 @@
+# 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.
+#
+# 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]
+envlist = black, cover, flake8, pylint, yamllint, safety
+skipsdist = true
+
+[tox:jenkins]
+toxworkdir = /tmp/.tox
+
+[testenv]
+basepython = python3.8
+setenv = VIRTUAL_ENV={envdir}
+ PYTHONDONTWRITEBYTECODE = 1
+deps = -r{toxinidir}/requirements.txt
+
+
+#######################################################################################
+[testenv:black]
+deps = black
+commands =
+ black --check --diff src/ tests/
+
+
+#######################################################################################
+[testenv:cover]
+deps = {[testenv]deps}
+ -r{toxinidir}/requirements-test.txt
+ coverage
+ nose2
+commands =
+ sh -c 'rm -f nosetests.xml'
+ coverage erase
+ nose2 -C --coverage src
+ coverage report --omit='*tests*'
+ coverage html -d ./cover --omit='*tests*'
+ coverage xml -o coverage.xml --omit=*tests*
+whitelist_externals = sh
+
+
+#######################################################################################
+[testenv:flake8]
+deps = flake8
+ flake8-import-order
+commands =
+ flake8 src/ tests/
+
+
+#######################################################################################
+[testenv:pylint]
+deps = {[testenv]deps}
+ -r{toxinidir}/requirements-test.txt
+ pylint
+commands =
+ pylint -E src/ tests/
+
+
+#######################################################################################
+[testenv:safety]
+setenv =
+ LC_ALL=C.UTF-8
+ LANG=C.UTF-8
+deps = {[testenv]deps}
+ safety
+commands =
+ - safety check --full-report
+
+
+#######################################################################################
+[testenv:yamllint]
+deps = {[testenv]deps}
+ -r{toxinidir}/requirements-test.txt
+ yamllint
+commands = yamllint .
+
+#######################################################################################
+[testenv:build]
+passenv=HTTP_PROXY HTTPS_PROXY NO_PROXY
+whitelist_externals =
+ charmcraft
+ sh
+commands =
+ charmcraft build
+ sh -c 'ubuntu_version=20.04; \
+ architectures="amd64-aarch64-arm64"; \
+ charm_name=`cat metadata.yaml | grep -E "^name: " | cut -f 2 -d " "`; \
+ mv $charm_name"_ubuntu-"$ubuntu_version-$architectures.charm $charm_name.charm'
+
+#######################################################################################
+[flake8]
+ignore =
+ W291,
+ W293,
+ W503,
+ E123,
+ E125,
+ E226,
+ E241,
+exclude =
+ .git,
+ __pycache__,
+ .tox,
+max-line-length = 120
+show-source = True
+builtins = _
+max-complexity = 10
+import-order-style = google