First Cut
This commit is contained in:
commit
0b2e540e14
9
charms/ovn-central-k8s/.flake8
Normal file
9
charms/ovn-central-k8s/.flake8
Normal file
@ -0,0 +1,9 @@
|
||||
[flake8]
|
||||
max-line-length = 99
|
||||
select: E,W,F,C,N
|
||||
exclude:
|
||||
venv
|
||||
.git
|
||||
build
|
||||
dist
|
||||
*.egg_info
|
10
charms/ovn-central-k8s/.gitignore
vendored
Normal file
10
charms/ovn-central-k8s/.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
venv/
|
||||
build/
|
||||
*.charm
|
||||
*.swp
|
||||
|
||||
.coverage
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.tox
|
||||
.stestr/
|
3
charms/ovn-central-k8s/.jujuignore
Normal file
3
charms/ovn-central-k8s/.jujuignore
Normal file
@ -0,0 +1,3 @@
|
||||
/venv
|
||||
*.py[cod]
|
||||
*.charm
|
3
charms/ovn-central-k8s/.stestr.conf
Normal file
3
charms/ovn-central-k8s/.stestr.conf
Normal file
@ -0,0 +1,3 @@
|
||||
[DEFAULT]
|
||||
test_path=./unit_tests
|
||||
top_dir=./
|
34
charms/ovn-central-k8s/CONTRIBUTING.md
Normal file
34
charms/ovn-central-k8s/CONTRIBUTING.md
Normal file
@ -0,0 +1,34 @@
|
||||
# sunbeam-ovn-central-operator
|
||||
|
||||
## Developing
|
||||
|
||||
Create and activate a virtualenv with the development requirements:
|
||||
|
||||
virtualenv -p python3 venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
## Code overview
|
||||
|
||||
TEMPLATE-TODO:
|
||||
One of the most important things a consumer of your charm (or library)
|
||||
needs to know is what set of functionality it provides. Which categories
|
||||
does it fit into? Which events do you listen to? Which libraries do you
|
||||
consume? Which ones do you export and how are they used?
|
||||
|
||||
## Intended use case
|
||||
|
||||
TEMPLATE-TODO:
|
||||
Why were these decisions made? What's the scope of your charm?
|
||||
|
||||
## Roadmap
|
||||
|
||||
If this Charm doesn't fulfill all of the initial functionality you were
|
||||
hoping for or planning on, please add a Roadmap or TODO here
|
||||
|
||||
## Testing
|
||||
|
||||
The Python operator framework includes a very nice harness for testing
|
||||
operator behaviour without full deployment. Just `run_tests`:
|
||||
|
||||
./run_tests
|
202
charms/ovn-central-k8s/LICENSE
Normal file
202
charms/ovn-central-k8s/LICENSE
Normal file
@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
24
charms/ovn-central-k8s/README.md
Normal file
24
charms/ovn-central-k8s/README.md
Normal file
@ -0,0 +1,24 @@
|
||||
# sunbeam-ovn-central-operator
|
||||
|
||||
## Description
|
||||
|
||||
TODO: Describe your charm in a few paragraphs of Markdown
|
||||
|
||||
## Usage
|
||||
|
||||
TODO: Provide high-level usage, such as required config or relations
|
||||
|
||||
|
||||
## Relations
|
||||
|
||||
TODO: Provide any relations which are provided or required by your charm
|
||||
|
||||
## OCI Images
|
||||
|
||||
TODO: Include a link to the default image your charm uses
|
||||
|
||||
## Contributing
|
||||
|
||||
Please see the [Juju SDK docs](https://juju.is/docs/sdk) for guidelines
|
||||
on enhancements to this charm following best practice guidelines, and
|
||||
`CONTRIBUTING.md` for developer guidance.
|
2
charms/ovn-central-k8s/actions.yaml
Normal file
2
charms/ovn-central-k8s/actions.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
# NOTE: no actions yet!
|
||||
{ }
|
19
charms/ovn-central-k8s/charmcraft.yaml
Normal file
19
charms/ovn-central-k8s/charmcraft.yaml
Normal file
@ -0,0 +1,19 @@
|
||||
# Learn more about charmcraft.yaml configuration at:
|
||||
# https://juju.is/docs/sdk/charmcraft-config
|
||||
type: "charm"
|
||||
bases:
|
||||
- build-on:
|
||||
- name: "ubuntu"
|
||||
channel: "20.04"
|
||||
run-on:
|
||||
- name: "ubuntu"
|
||||
channel: "20.04"
|
||||
parts:
|
||||
charm:
|
||||
build-packages:
|
||||
- git
|
||||
- libffi-dev
|
||||
- libssl-dev
|
||||
charm-python-packages:
|
||||
- setuptools < 58
|
||||
- cryptography < 3.4
|
29
charms/ovn-central-k8s/config.yaml
Normal file
29
charms/ovn-central-k8s/config.yaml
Normal file
@ -0,0 +1,29 @@
|
||||
options:
|
||||
ovsdb-server-election-timer:
|
||||
default: 4
|
||||
type: int
|
||||
description: |
|
||||
Raft leader election timeout in seconds. The charm allows a value between
|
||||
1 and 60 seconds.
|
||||
.
|
||||
The Open vSwitch ovsdb-server default of 1 second may not be sufficient
|
||||
for a loaded cluster where the database server may be too busy serving
|
||||
requests to respond to elections in time.
|
||||
.
|
||||
Using a higher value will increase the time to discover a real failure,
|
||||
but you must weigh that against the risk of spurious leader flapping and
|
||||
the unwanted churn that entails.
|
||||
.
|
||||
NOTE: The ovsdb-server will refuse to decrease or increase the value of
|
||||
this timer more than 2x the current value. The charm will compensate for
|
||||
this and decrease / increase the timer in increments, but care should be
|
||||
taken to not decrease / increase the value too much in one operation.
|
||||
ovsdb-server-inactivity-probe:
|
||||
default: 60
|
||||
type: int
|
||||
description: |
|
||||
Maximum number of seconds of idle time on connection to client before
|
||||
sending an inactivity probe message.
|
||||
|
||||
The Open vSwitch ovsdb-server default of 5 seconds may not be sufficient
|
||||
depending on type and load of the CMS you want to connect to OVN.
|
@ -0,0 +1,241 @@
|
||||
# Copyright 2021 Canonical Ltd.
|
||||
# See LICENSE file for licensing details.
|
||||
|
||||
"""# KubernetesServicePatch Library.
|
||||
|
||||
This library is designed to enable developers to more simply patch the Kubernetes Service created
|
||||
by Juju during the deployment of a sidecar charm. When sidecar charms are deployed, Juju creates a
|
||||
service named after the application in the namespace (named after the Juju model). This service by
|
||||
default contains a "placeholder" port, which is 65536/TCP.
|
||||
|
||||
When modifying the default set of resources managed by Juju, one must consider the lifecycle of the
|
||||
charm. In this case, any modifications to the default service (created during deployment), will
|
||||
be overwritten during a charm upgrade.
|
||||
|
||||
When intialised, this library binds a handler to the parent charm's `install` and `upgrade_charm`
|
||||
events which applies the patch to the cluster. This should ensure that the service ports are
|
||||
correct throughout the charm's life.
|
||||
|
||||
The constructor simply takes a reference to the parent charm, and a list of tuples that each define
|
||||
a port for the service, where each tuple contains:
|
||||
|
||||
- a name for the port
|
||||
- port for the service to listen on
|
||||
- optionally: a targetPort for the service (the port in the container!)
|
||||
- optionally: a nodePort for the service (for NodePort or LoadBalancer services only!)
|
||||
- optionally: a name of the service (in case service name needs to be patched as well)
|
||||
|
||||
## Getting Started
|
||||
|
||||
To get started using the library, you just need to fetch the library using `charmcraft`. **Note
|
||||
that you also need to add `lightkube` and `lightkube-models` to your charm's `requirements.txt`.**
|
||||
|
||||
```shell
|
||||
cd some-charm
|
||||
charmcraft fetch-lib charms.observability_libs.v0.kubernetes_service_patch
|
||||
echo <<-EOF >> requirements.txt
|
||||
lightkube
|
||||
lightkube-models
|
||||
EOF
|
||||
```
|
||||
|
||||
Then, to initialise the library:
|
||||
|
||||
For ClusterIP services:
|
||||
```python
|
||||
# ...
|
||||
from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
|
||||
|
||||
class SomeCharm(CharmBase):
|
||||
def __init__(self, *args):
|
||||
# ...
|
||||
self.service_patcher = KubernetesServicePatch(self, [(f"{self.app.name}", 8080)])
|
||||
# ...
|
||||
```
|
||||
|
||||
For LoadBalancer/NodePort services:
|
||||
```python
|
||||
# ...
|
||||
from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
|
||||
|
||||
class SomeCharm(CharmBase):
|
||||
def __init__(self, *args):
|
||||
# ...
|
||||
self.service_patcher = KubernetesServicePatch(
|
||||
self, [(f"{self.app.name}", 443, 443, 30666)], "LoadBalancer"
|
||||
)
|
||||
# ...
|
||||
```
|
||||
|
||||
Additionally, you may wish to use mocks in your charm's unit testing to ensure that the library
|
||||
does not try to make any API calls, or open any files during testing that are unlikely to be
|
||||
present, and could break your tests. The easiest way to do this is during your test `setUp`:
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
@patch("charm.KubernetesServicePatch", lambda x, y: None)
|
||||
def setUp(self, *unused):
|
||||
self.harness = Harness(SomeCharm)
|
||||
# ...
|
||||
```
|
||||
"""
|
||||
|
||||
import logging
|
||||
from types import MethodType
|
||||
from typing import Literal, Sequence, Tuple, Union
|
||||
|
||||
from lightkube import ApiError, Client
|
||||
from lightkube.models.core_v1 import ServicePort, ServiceSpec
|
||||
from lightkube.models.meta_v1 import ObjectMeta
|
||||
from lightkube.resources.core_v1 import Service
|
||||
from lightkube.types import PatchType
|
||||
from ops.charm import CharmBase
|
||||
from ops.framework import Object
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "0042f86d0a874435adef581806cddbbb"
|
||||
|
||||
# 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 = 5
|
||||
|
||||
PortDefinition = Union[Tuple[str, int], Tuple[str, int, int], Tuple[str, int, int, int]]
|
||||
ServiceType = Literal["ClusterIP", "LoadBalancer"]
|
||||
|
||||
|
||||
class KubernetesServicePatch(Object):
|
||||
"""A utility for patching the Kubernetes service set up by Juju."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
charm: CharmBase,
|
||||
ports: Sequence[PortDefinition],
|
||||
service_name: str = None,
|
||||
service_type: ServiceType = "ClusterIP",
|
||||
):
|
||||
"""Constructor for KubernetesServicePatch.
|
||||
|
||||
Args:
|
||||
charm: the charm that is instantiating the library.
|
||||
ports: a list of tuples (name, port, targetPort, nodePort) for every service port.
|
||||
service_name: allows setting custom name to the patched service. If none given,
|
||||
application name will be used.
|
||||
service_type: desired type of K8s service. Default value is in line with ServiceSpec's
|
||||
default value.
|
||||
"""
|
||||
super().__init__(charm, "kubernetes-service-patch")
|
||||
self.charm = charm
|
||||
self.service_name = service_name if service_name else self._app
|
||||
self.service = self._service_object(ports, service_name, service_type)
|
||||
|
||||
# Make mypy type checking happy that self._patch is a method
|
||||
assert isinstance(self._patch, MethodType)
|
||||
# Ensure this patch is applied during the 'install' and 'upgrade-charm' events
|
||||
self.framework.observe(charm.on.install, self._patch)
|
||||
self.framework.observe(charm.on.upgrade_charm, self._patch)
|
||||
|
||||
def _service_object(
|
||||
self,
|
||||
ports: Sequence[PortDefinition],
|
||||
service_name: str = None,
|
||||
service_type: ServiceType = "ClusterIP",
|
||||
) -> Service:
|
||||
"""Creates a valid Service representation for Alertmanager.
|
||||
|
||||
Args:
|
||||
ports: a list of tuples of the form (name, port) or (name, port, targetPort)
|
||||
or (name, port, targetPort, nodePort) for every service port. If the 'targetPort'
|
||||
is omitted, it is assumed to be equal to 'port', with the exception of NodePort
|
||||
and LoadBalancer services, where all port numbers have to be specified.
|
||||
service_name: allows setting custom name to the patched service. If none given,
|
||||
application name will be used.
|
||||
service_type: desired type of K8s service. Default value is in line with ServiceSpec's
|
||||
default value.
|
||||
|
||||
Returns:
|
||||
Service: A valid representation of a Kubernetes Service with the correct ports.
|
||||
"""
|
||||
if not service_name:
|
||||
service_name = self._app
|
||||
return Service(
|
||||
apiVersion="v1",
|
||||
kind="Service",
|
||||
metadata=ObjectMeta(
|
||||
namespace=self._namespace,
|
||||
name=service_name,
|
||||
labels={"app.kubernetes.io/name": service_name},
|
||||
),
|
||||
spec=ServiceSpec(
|
||||
selector={"app.kubernetes.io/name": service_name},
|
||||
ports=[
|
||||
ServicePort(
|
||||
name=p[0],
|
||||
port=p[1],
|
||||
targetPort=p[2] if len(p) > 2 else p[1], # type: ignore[misc]
|
||||
nodePort=p[3] if len(p) > 3 else None, # type: ignore[arg-type, misc]
|
||||
)
|
||||
for p in ports
|
||||
],
|
||||
type=service_type,
|
||||
),
|
||||
)
|
||||
|
||||
def _patch(self, _) -> None:
|
||||
"""Patch the Kubernetes service created by Juju to map the correct port.
|
||||
|
||||
Raises:
|
||||
PatchFailed: if patching fails due to lack of permissions, or otherwise.
|
||||
"""
|
||||
if not self.charm.unit.is_leader():
|
||||
return
|
||||
|
||||
client = Client()
|
||||
try:
|
||||
client.patch(Service, self._app, self.service, patch_type=PatchType.MERGE)
|
||||
except ApiError as e:
|
||||
if e.status.code == 403:
|
||||
logger.error("Kubernetes service patch failed: `juju trust` this application.")
|
||||
else:
|
||||
logger.error("Kubernetes service patch failed: %s", str(e))
|
||||
else:
|
||||
logger.info("Kubernetes service '%s' patched successfully", self._app)
|
||||
|
||||
def is_patched(self) -> bool:
|
||||
"""Reports if the service patch has been applied.
|
||||
|
||||
Returns:
|
||||
bool: A boolean indicating if the service patch has been applied.
|
||||
"""
|
||||
client = Client()
|
||||
# Get the relevant service from the cluster
|
||||
service = client.get(Service, name=self.service_name, namespace=self._namespace)
|
||||
# Construct a list of expected ports, should the patch be applied
|
||||
expected_ports = [(p.port, p.targetPort) for p in self.service.spec.ports]
|
||||
# Construct a list in the same manner, using the fetched service
|
||||
fetched_ports = [(p.port, p.targetPort) for p in service.spec.ports] # type: ignore[attr-defined] # noqa: E501
|
||||
return expected_ports == fetched_ports
|
||||
|
||||
@property
|
||||
def _app(self) -> str:
|
||||
"""Name of the current Juju application.
|
||||
|
||||
Returns:
|
||||
str: A string containing the name of the current Juju application.
|
||||
"""
|
||||
return self.charm.app.name
|
||||
|
||||
@property
|
||||
def _namespace(self) -> str:
|
||||
"""The Kubernetes namespace we're running in.
|
||||
|
||||
Returns:
|
||||
str: A string containing the name of the current Kubernetes namespace.
|
||||
"""
|
||||
with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") as f:
|
||||
return f.read().strip()
|
@ -0,0 +1,202 @@
|
||||
"""TODO: Add a proper docstring here.
|
||||
|
||||
This is a placeholder docstring for this charm library. Docstrings are
|
||||
presented on Charmhub and updated whenever you push a new version of the
|
||||
library.
|
||||
|
||||
Complete documentation about creating and documenting libraries can be found
|
||||
in the SDK docs at https://juju.is/docs/sdk/libraries.
|
||||
|
||||
See `charmcraft publish-lib` and `charmcraft fetch-lib` for details of how to
|
||||
share and consume charm libraries. They serve to enhance collaboration
|
||||
between charmers. Use a charmer's libraries for classes that handle
|
||||
integration with their charm.
|
||||
|
||||
Bear in mind that new revisions of the different major API versions (v0, v1,
|
||||
v2 etc) are maintained independently. You can continue to update v0 and v1
|
||||
after you have pushed v3.
|
||||
|
||||
Markdown is supported, following the CommonMark specification.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import typing
|
||||
from ops.framework import (
|
||||
StoredState,
|
||||
EventBase,
|
||||
ObjectEvents,
|
||||
EventSource,
|
||||
Object,
|
||||
)
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "19e5a5857acd4a94a4a759d173d18232"
|
||||
|
||||
# Increment this major API version when introducing breaking changes
|
||||
LIBAPI = 0
|
||||
|
||||
# Increment this PATCH version before using `charmcraft publish-lib` or reset
|
||||
# to 0 if you are raising the major API version
|
||||
LIBPATCH = 1
|
||||
|
||||
|
||||
# TODO: add your code here! Happy coding!
|
||||
class OVSDBCMSConnectedEvent(EventBase):
|
||||
"""OVSDBCMS connected Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class OVSDBCMSReadyEvent(EventBase):
|
||||
"""OVSDBCMS ready for use Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class OVSDBCMSGoneAwayEvent(EventBase):
|
||||
"""OVSDBCMS relation has gone-away Event"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class OVSDBCMSServerEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
connected = EventSource(OVSDBCMSConnectedEvent)
|
||||
ready = EventSource(OVSDBCMSReadyEvent)
|
||||
goneaway = EventSource(OVSDBCMSGoneAwayEvent)
|
||||
|
||||
|
||||
class OVSDBCMSRequires(Object):
|
||||
"""
|
||||
OVSDBCMSRequires class
|
||||
"""
|
||||
|
||||
on = OVSDBCMSServerEvents()
|
||||
_stored = StoredState()
|
||||
|
||||
def __init__(self, charm, relation_name: str):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_ovsdb_cms_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_ovsdb_cms_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_departed,
|
||||
self._on_ovsdb_cms_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_ovsdb_cms_relation_broken,
|
||||
)
|
||||
|
||||
def _on_ovsdb_cms_relation_joined(self, event):
|
||||
"""OVSDBCMS relation joined."""
|
||||
logging.debug("OVSDBCMSRequires on_joined")
|
||||
self.on.connected.emit()
|
||||
|
||||
def bound_addresses(self):
|
||||
return self.get_all_unit_values("bound-address")
|
||||
|
||||
def remote_ready(self):
|
||||
return all(self.bound_addresses())
|
||||
|
||||
def _on_ovsdb_cms_relation_changed(self, event):
|
||||
"""OVSDBCMS relation changed."""
|
||||
logging.debug("OVSDBCMSRequires on_changed")
|
||||
if self.remote_ready():
|
||||
self.on.ready.emit()
|
||||
|
||||
def _on_ovsdb_cms_relation_broken(self, event):
|
||||
"""OVSDBCMS relation broken."""
|
||||
logging.debug("OVSDBCMSRequires on_broken")
|
||||
self.on.goneaway.emit()
|
||||
|
||||
def get_all_unit_values(self, key: str) -> typing.List[str]:
|
||||
"""Retrieve value for key from all related units."""
|
||||
values = []
|
||||
relation = self.framework.model.get_relation(self.relation_name)
|
||||
for unit in relation.units:
|
||||
values.append(relation.data[unit].get(key))
|
||||
return values
|
||||
|
||||
|
||||
|
||||
class OVSDBCMSClientConnectedEvent(EventBase):
|
||||
"""OVSDBCMS connected Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class OVSDBCMSClientReadyEvent(EventBase):
|
||||
"""OVSDBCMS ready for use Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class OVSDBCMSClientGoneAwayEvent(EventBase):
|
||||
"""OVSDBCMS relation has gone-away Event"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class OVSDBCMSClientEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
connected = EventSource(OVSDBCMSClientConnectedEvent)
|
||||
ready = EventSource(OVSDBCMSClientReadyEvent)
|
||||
goneaway = EventSource(OVSDBCMSClientGoneAwayEvent)
|
||||
|
||||
|
||||
class OVSDBCMSProvides(Object):
|
||||
"""
|
||||
OVSDBCMSProvides class
|
||||
"""
|
||||
|
||||
on = OVSDBCMSClientEvents()
|
||||
_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_ovsdb_cms_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_ovsdb_cms_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_ovsdb_cms_relation_broken,
|
||||
)
|
||||
|
||||
def _on_ovsdb_cms_relation_joined(self, event):
|
||||
"""Handle ovsdb-cms joined."""
|
||||
logging.debug("OVSDBCMSProvides on_joined")
|
||||
self.on.connected.emit()
|
||||
|
||||
def _on_ovsdb_cms_relation_changed(self, event):
|
||||
"""Handle ovsdb-cms changed."""
|
||||
logging.debug("OVSDBCMSProvides on_changed")
|
||||
self.on.ready.emit()
|
||||
|
||||
def _on_ovsdb_cms_relation_broken(self, event):
|
||||
"""Handle ovsdb-cms broken."""
|
||||
logging.debug("OVSDBCMSProvides on_departed")
|
||||
self.on.goneaway.emit()
|
||||
|
||||
def set_unit_data(self, settings: typing.Dict[str, str]) -> None:
|
||||
"""Publish settings on the peer unit data bag."""
|
||||
relation = self.framework.model.get_relation(self.relation_name)
|
||||
for k, v in settings.items():
|
||||
relation.data[self.model.unit][k] = v
|
||||
|
60
charms/ovn-central-k8s/metadata.yaml
Normal file
60
charms/ovn-central-k8s/metadata.yaml
Normal file
@ -0,0 +1,60 @@
|
||||
name: sunbeam-ovn-central-operator
|
||||
summary: Open Virtual Network for Open vSwitch
|
||||
maintainer: OpenStack Charmers <openstack-charmers@lists.ubuntu.com>
|
||||
description: |
|
||||
Principal charm that deploys ovn-northd, the OVN central control daemon,
|
||||
and ovsdb-server, the Open vSwitch Database (OVSDB).
|
||||
|
||||
The ovn-northd daemon is responsible for translating the high-level OVN
|
||||
configuration into logical configuration consumable by daemons such as
|
||||
ovn-controller.
|
||||
|
||||
The ovn-northd process talks to OVN Northbound- and Southbound- databases.
|
||||
|
||||
The ovsdb-server exposes endpoints over relations implemented by the ovsdb
|
||||
interface.
|
||||
|
||||
The charm supports clustering of the OVSDB, you must have a odd number of
|
||||
units for this to work. Note that write performance decreases as you
|
||||
increase the number of units.
|
||||
|
||||
Running multiple ovn-northd daemons is supported and they will operate in
|
||||
active/passive mode. The daemon uses a locking feature in the OVSDB to
|
||||
automatically choose a single active instance.
|
||||
tags:
|
||||
- networking
|
||||
|
||||
containers:
|
||||
ovn-sb-db-server:
|
||||
resource: ovn-sb-db-server-image
|
||||
ovn-nb-db-server:
|
||||
resource: ovn-nb-db-server-image
|
||||
ovn-northd:
|
||||
resource: ovn-northd-image
|
||||
|
||||
resources:
|
||||
ovn-sb-db-server-image:
|
||||
type: oci-image
|
||||
description: OCI image
|
||||
ovn-nb-db-server-image:
|
||||
type: oci-image
|
||||
description: OCI image
|
||||
ovn-northd-image:
|
||||
type: oci-image
|
||||
description: OCI image
|
||||
|
||||
requires:
|
||||
certificates:
|
||||
interface: tls-certificates
|
||||
|
||||
provides:
|
||||
ovsdb:
|
||||
interface: ovsdb
|
||||
ovsdb-cms:
|
||||
interface: ovsdb-cms
|
||||
ovsdb-server:
|
||||
interface: ovsdb-cluster
|
||||
|
||||
peers:
|
||||
peers:
|
||||
interface: ovn-central-peer
|
3
charms/ovn-central-k8s/requirements-dev.txt
Normal file
3
charms/ovn-central-k8s/requirements-dev.txt
Normal file
@ -0,0 +1,3 @@
|
||||
-r requirements.txt
|
||||
coverage
|
||||
flake8
|
11
charms/ovn-central-k8s/requirements.txt
Normal file
11
charms/ovn-central-k8s/requirements.txt
Normal file
@ -0,0 +1,11 @@
|
||||
ops
|
||||
jinja2
|
||||
git+https://opendev.org/openstack/charm-ops-openstack#egg=ops_openstack
|
||||
git+https://github.com/openstack-charmers/advanced-sunbeam-openstack#egg=advanced_sunbeam_openstack
|
||||
lightkube
|
||||
# These are only needeed if the charm relates to ceph
|
||||
git+https://github.com/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client
|
||||
# Charmhelpers is only present as interface_ceph_client uses it.
|
||||
git+https://github.com/juju/charm-helpers.git#egg=charmhelpers
|
||||
cryptography < 3.4
|
||||
git+https://opendev.org/openstack/charm-ops-interface-tls-certificates#egg=interface_tls_certificates
|
17
charms/ovn-central-k8s/run_tests
Executable file
17
charms/ovn-central-k8s/run_tests
Executable file
@ -0,0 +1,17 @@
|
||||
#!/bin/sh -e
|
||||
# Copyright 2022 liam
|
||||
# See LICENSE file for licensing details.
|
||||
|
||||
if [ -z "$VIRTUAL_ENV" -a -d venv/ ]; then
|
||||
. venv/bin/activate
|
||||
fi
|
||||
|
||||
if [ -z "$PYTHONPATH" ]; then
|
||||
export PYTHONPATH="lib:src"
|
||||
else
|
||||
export PYTHONPATH="lib:src:$PYTHONPATH"
|
||||
fi
|
||||
|
||||
flake8
|
||||
coverage run --branch --source=src -m unittest -v "$@"
|
||||
coverage report -m
|
470
charms/ovn-central-k8s/src/charm.py
Executable file
470
charms/ovn-central-k8s/src/charm.py
Executable file
@ -0,0 +1,470 @@
|
||||
#!/usr/bin/env python3
|
||||
"""OVN Central Operator Charm.
|
||||
|
||||
This charm provide Glance services as part of an OpenStack deployment
|
||||
"""
|
||||
|
||||
import os
|
||||
import ovn
|
||||
import ovsdb as ch_ovsdb
|
||||
import ipaddress
|
||||
import logging
|
||||
import itertools
|
||||
from typing import List
|
||||
|
||||
import ops.charm
|
||||
from ops.framework import StoredState
|
||||
from ops.main import main
|
||||
|
||||
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.relation_handlers as sunbeam_rhandlers
|
||||
import advanced_sunbeam_openstack.config_contexts as sunbeam_ctxts
|
||||
import advanced_sunbeam_openstack.container_handlers as sunbeam_chandlers
|
||||
|
||||
import charms.sunbeam_ovn_central_operator.v0.ovsdb as ovsdb
|
||||
|
||||
from charms.observability_libs.v0.kubernetes_service_patch \
|
||||
import KubernetesServicePatch
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
OVN_SB_DB_CONTAINER = "ovn-sb-db-server"
|
||||
OVN_NB_DB_CONTAINER = "ovn-nb-db-server"
|
||||
OVN_NORTHD_CONTAINER = "ovn-northd"
|
||||
|
||||
|
||||
|
||||
|
||||
class OVNDBConfigContext(sunbeam_ctxts.ConfigContext):
|
||||
|
||||
def context(self) -> dict:
|
||||
return {
|
||||
'is_charm_leader': self.charm.unit.is_leader(),
|
||||
'ovn_key': '/etc/ovn/key_host',
|
||||
'ovn_cert': '/etc/ovn/cert_host',
|
||||
'ovn_ca_cert': '/etc/ovn/ovn-central.crt'}
|
||||
|
||||
|
||||
class OVNPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
|
||||
|
||||
def init_service(self, context: sunbeam_core.OPSCharmContexts) -> None:
|
||||
"""Initialise service ready for use.
|
||||
|
||||
Write configuration files to the container and record
|
||||
that service is ready for us.
|
||||
|
||||
NOTE: Override default to services being automatically started
|
||||
"""
|
||||
self.setup_dirs()
|
||||
self.write_config(context)
|
||||
self._state.service_ready = True
|
||||
|
||||
@property
|
||||
def directories(self):
|
||||
return [
|
||||
sunbeam_chandlers.ContainerDir(
|
||||
'/etc/ovn',
|
||||
'root',
|
||||
'root'),
|
||||
sunbeam_chandlers.ContainerDir(
|
||||
'/run/ovn',
|
||||
'root',
|
||||
'root'),
|
||||
sunbeam_chandlers.ContainerDir(
|
||||
'/var/lib/ovn',
|
||||
'root',
|
||||
'root'),
|
||||
sunbeam_chandlers.ContainerDir(
|
||||
'/var/log/ovn',
|
||||
'root',
|
||||
'root')]
|
||||
|
||||
def default_container_configs(self):
|
||||
return [
|
||||
sunbeam_core.ContainerConfigFile(
|
||||
[self.container_name],
|
||||
'/etc/ovn/key_host',
|
||||
'root',
|
||||
'root'),
|
||||
sunbeam_core.ContainerConfigFile(
|
||||
[self.container_name],
|
||||
'/etc/ovn/cert_host',
|
||||
'root',
|
||||
'root'),
|
||||
sunbeam_core.ContainerConfigFile(
|
||||
[self.container_name],
|
||||
'/etc/ovn/ovn-central.crt',
|
||||
'root',
|
||||
'root')]
|
||||
|
||||
|
||||
class OVNNorthBPebbleHandler(OVNPebbleHandler):
|
||||
|
||||
def default_container_configs(self):
|
||||
_cc = super().default_container_configs()
|
||||
_cc.append(
|
||||
sunbeam_core.ContainerConfigFile(
|
||||
[self.container_name],
|
||||
'/etc/ovn/ovn-northd-db-params.conf',
|
||||
'root',
|
||||
'root'))
|
||||
_cc.append(
|
||||
sunbeam_core.ContainerConfigFile(
|
||||
[self.container_name],
|
||||
'/root/ovn-northd-wrapper.sh',
|
||||
'root',
|
||||
'root'))
|
||||
return _cc
|
||||
|
||||
def get_layer(self) -> dict:
|
||||
return {
|
||||
"summary": f"OVN Northd layer",
|
||||
"description": "pebble config layer for OVN NorthD",
|
||||
"services": {
|
||||
f"ovn-northd": {
|
||||
"override": "replace",
|
||||
"summary": f"OVN Northd",
|
||||
"command": "bash /root/ovn-northd-wrapper.sh",
|
||||
"startup": "disabled",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class OVNNorthBDBPebbleHandler(OVNPebbleHandler):
|
||||
|
||||
def default_container_configs(self):
|
||||
_cc = super().default_container_configs()
|
||||
_cc.append(
|
||||
sunbeam_core.ContainerConfigFile(
|
||||
[self.container_name],
|
||||
'/root/ovn-nb-db-server-wrapper.sh',
|
||||
'root',
|
||||
'root'))
|
||||
_cc.append(
|
||||
sunbeam_core.ContainerConfigFile(
|
||||
[self.container_name],
|
||||
'/root/ovn-nb-cluster-join.sh',
|
||||
'root',
|
||||
'root'))
|
||||
return _cc
|
||||
|
||||
def get_layer(self) -> dict:
|
||||
return {
|
||||
"summary": f"OVN North Bound DB layer",
|
||||
"description": "pebble config layer for OVN North DB",
|
||||
"services": {
|
||||
f"ovn-nb-db-server": {
|
||||
"override": "replace",
|
||||
"summary": f"OVN North Bound DB Server",
|
||||
"command": "bash /root/ovn-nb-db-server-wrapper.sh",
|
||||
"startup": "disabled",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class OVNSouthBDBPebbleHandler(OVNPebbleHandler):
|
||||
|
||||
def default_container_configs(self):
|
||||
_cc = super().default_container_configs()
|
||||
_cc.append(
|
||||
sunbeam_core.ContainerConfigFile(
|
||||
[self.container_name],
|
||||
'/root/ovn-sb-db-server-wrapper.sh',
|
||||
'root',
|
||||
'root'))
|
||||
_cc.append(
|
||||
sunbeam_core.ContainerConfigFile(
|
||||
[self.container_name],
|
||||
'/root/ovn-sb-cluster-join.sh',
|
||||
'root',
|
||||
'root'))
|
||||
return _cc
|
||||
|
||||
def get_layer(self) -> dict:
|
||||
return {
|
||||
"summary": f"OVN South Bound DB layer",
|
||||
"description": "pebble config layer for OVN South DB",
|
||||
"services": {
|
||||
f"ovn-sb-db-server": {
|
||||
"override": "replace",
|
||||
"summary": f"OVN South Bound DB Server",
|
||||
"command": "bash /root/ovn-sb-db-server-wrapper.sh",
|
||||
"startup": "disabled",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class OVNCentralOperatorCharm(sunbeam_charm.OSBaseOperatorCharm):
|
||||
"""Charm the service."""
|
||||
|
||||
_state = StoredState()
|
||||
|
||||
def __init__(self, framework):
|
||||
super().__init__(framework)
|
||||
self.service_patcher = KubernetesServicePatch(
|
||||
self,
|
||||
[
|
||||
('northbound', 6643),
|
||||
('southbound', 6644),
|
||||
]
|
||||
)
|
||||
|
||||
def get_pebble_handlers(self):
|
||||
pebble_handlers = [
|
||||
OVNNorthBPebbleHandler(
|
||||
self,
|
||||
OVN_NORTHD_CONTAINER,
|
||||
'ovn-northd',
|
||||
self.container_configs,
|
||||
self.template_dir,
|
||||
self.openstack_release,
|
||||
self.configure_charm),
|
||||
OVNSouthBDBPebbleHandler(
|
||||
self,
|
||||
OVN_SB_DB_CONTAINER,
|
||||
'ovn-sb-db-server',
|
||||
self.container_configs,
|
||||
self.template_dir,
|
||||
self.openstack_release,
|
||||
self.configure_charm),
|
||||
OVNNorthBDBPebbleHandler(
|
||||
self,
|
||||
OVN_NB_DB_CONTAINER,
|
||||
'ovn-nb-db-server',
|
||||
self.container_configs,
|
||||
self.template_dir,
|
||||
self.openstack_release,
|
||||
self.configure_charm)]
|
||||
return pebble_handlers
|
||||
|
||||
def get_relation_handlers(self, handlers=None) -> List[
|
||||
sunbeam_rhandlers.RelationHandler]:
|
||||
"""Relation handlers for the service."""
|
||||
handlers = handlers or []
|
||||
if self.can_add_handler('peers', handlers):
|
||||
self.peers = sunbeam_rhandlers.OVNDBClusterPeerHandler(
|
||||
self,
|
||||
'peers',
|
||||
self.configure_charm)
|
||||
handlers.append(self.peers)
|
||||
if self.can_add_handler('ovsdb-cms', handlers):
|
||||
self.ovsdb_cms = sunbeam_rhandlers.OVSDBCMSProvidesHandler(
|
||||
self,
|
||||
'ovsdb-cms',
|
||||
self.configure_charm)
|
||||
handlers.append(self.ovsdb_cms)
|
||||
handlers = super().get_relation_handlers(handlers)
|
||||
return handlers
|
||||
|
||||
@property
|
||||
def config_contexts(self) -> List[sunbeam_ctxts.ConfigContext]:
|
||||
"""Configuration contexts for the operator."""
|
||||
contexts = super().config_contexts
|
||||
contexts.append(
|
||||
OVNDBConfigContext(self, "ovs_db"))
|
||||
return contexts
|
||||
|
||||
def ovn_rundir(self):
|
||||
return '/var/run/ovn'
|
||||
|
||||
def get_pebble_executor(self, container_name):
|
||||
container = self.unit.get_container(
|
||||
container_name)
|
||||
def _run_via_pebble(*args):
|
||||
process = container.exec(list(args), timeout=5*60)
|
||||
out, warnings = process.wait_output()
|
||||
if warnings:
|
||||
for line in warnings.splitlines():
|
||||
logger.warning('CMD Out: %s', line.strip())
|
||||
return out
|
||||
return _run_via_pebble
|
||||
|
||||
def cluster_status(self, db, cmd_executor):
|
||||
"""OVN version agnostic cluster_status helper.
|
||||
|
||||
:param db: Database to operate on
|
||||
:type db: str
|
||||
:returns: Object describing the cluster status or None
|
||||
:rtype: Optional[ch_ovn.OVNClusterStatus]
|
||||
"""
|
||||
try:
|
||||
# The charm will attempt to retrieve cluster status before OVN
|
||||
# is clustered and while units are paused, so we need to handle
|
||||
# errors from this call gracefully.
|
||||
return ovn.cluster_status(
|
||||
db,
|
||||
rundir=self.ovn_rundir(),
|
||||
cmd_executor=cmd_executor)
|
||||
except (ValueError) as e:
|
||||
logging.error('Unable to get cluster status, ovsdb-server '
|
||||
'not ready yet?: {}'.format(e))
|
||||
return
|
||||
|
||||
def configure_ovn_listener(self, db, port_map):
|
||||
"""Create or update OVN listener configuration.
|
||||
|
||||
:param db: Database to operate on, 'nb' or 'sb'
|
||||
:type db: str
|
||||
:param port_map: Dictionary with port number and associated settings
|
||||
:type port_map: Dict[int,Dict[str,str]]
|
||||
:raises: ValueError
|
||||
"""
|
||||
if db == 'nb':
|
||||
executor = self.get_pebble_executor(OVN_NB_DB_CONTAINER)
|
||||
elif db == 'sb':
|
||||
executor = self.get_pebble_executor(OVN_SB_DB_CONTAINER)
|
||||
status = self.cluster_status('ovn{}_db'.format(db), cmd_executor=executor)
|
||||
if status and status.is_cluster_leader:
|
||||
logging.debug('configure_ovn_listener is_cluster_leader {}'.format(db))
|
||||
connections = ch_ovsdb.SimpleOVSDB(
|
||||
'ovn-{}ctl'.format(db),
|
||||
cmd_executor=executor).connection
|
||||
for port, settings in port_map.items():
|
||||
logging.debug('port {} {}'.format(port, settings))
|
||||
# discover and create any non-existing listeners first
|
||||
for connection in connections.find(
|
||||
'target="pssl:{}"'.format(port)):
|
||||
logging.debug('Found port {}'.format(port))
|
||||
break
|
||||
else:
|
||||
logging.debug('Create port {}'.format(port))
|
||||
executor(
|
||||
'ovn-{}ctl'.format(db),
|
||||
'--',
|
||||
'--id=@connection',
|
||||
'create', 'connection',
|
||||
'target="pssl:{}"'.format(port),
|
||||
'--',
|
||||
'add', '{}_Global'.format(db.upper()),
|
||||
'.', 'connections', '@connection')
|
||||
# set/update connection settings
|
||||
for connection in connections.find(
|
||||
'target="pssl:{}"'.format(port)):
|
||||
for k, v in settings.items():
|
||||
logging.debug(
|
||||
'set {} {} {}'
|
||||
.format(str(connection['_uuid']), k, v))
|
||||
connections.set(str(connection['_uuid']), k, v)
|
||||
|
||||
def get_named_pebble_handlers(self, container_names):
|
||||
# XXX Move to ASO
|
||||
return [
|
||||
h
|
||||
for h in self.pebble_handlers
|
||||
if h.container_name in container_names
|
||||
]
|
||||
|
||||
def configure_charm(self, event: ops.framework.EventBase) -> None:
|
||||
"""Catchall handler to configure charm services.
|
||||
|
||||
"""
|
||||
if not self.unit.is_leader():
|
||||
if not self.is_leader_ready():
|
||||
self.unit.status = ops.model.WaitingStatus(
|
||||
"Waiting for leader to be ready")
|
||||
return
|
||||
missing_leader_data = [
|
||||
k for k in ['nb_cid', 'nb_cid']
|
||||
if not self.leader_get(k)]
|
||||
if missing_leader_data:
|
||||
logging.debug(f"missing {missing_leader_data} from leader")
|
||||
self.unit.status = ops.model.WaitingStatus(
|
||||
"Waiting for data from leader")
|
||||
return
|
||||
logging.debug("Remote leader is ready and has supplied all data needed")
|
||||
|
||||
if not self.relation_handlers_ready():
|
||||
logging.debug("Aborting charm relations not ready")
|
||||
return
|
||||
|
||||
# Render Config in all containers but init should *NOT* start
|
||||
# the service.
|
||||
for ph in self.pebble_handlers:
|
||||
if ph.pebble_ready:
|
||||
logging.debug(f"Running init for {ph.service_name}")
|
||||
ph.init_service(self.contexts())
|
||||
else:
|
||||
logging.debug(
|
||||
f"Not running init for {ph.service_name},"
|
||||
" container not ready")
|
||||
|
||||
if self.unit.is_leader():
|
||||
# Start services in North/South containers on lead unit
|
||||
logging.debug("Starting services in DB containers")
|
||||
for ph in self.get_named_pebble_handlers([OVN_SB_DB_CONTAINER, OVN_NB_DB_CONTAINER]):
|
||||
ph.start_service()
|
||||
# Attempt to setup listers etc
|
||||
self.configure_ovn()
|
||||
nb_status = self.cluster_status(
|
||||
'ovnnb_db',
|
||||
self.get_pebble_executor(OVN_NB_DB_CONTAINER))
|
||||
sb_status = self.cluster_status(
|
||||
'ovnsb_db',
|
||||
self.get_pebble_executor(OVN_SB_DB_CONTAINER))
|
||||
logging.debug("Telling peers leader is ready and cluster ids")
|
||||
self.set_leader_ready()
|
||||
self.leader_set({
|
||||
'nb_cid': str(nb_status.cluster_id),
|
||||
'sb_cid': str(sb_status.cluster_id),
|
||||
})
|
||||
self.unit.status = ops.model.ActiveStatus()
|
||||
else:
|
||||
logging.debug("Attempting to join OVN_Northbound cluster")
|
||||
container = self.unit.get_container(OVN_NB_DB_CONTAINER)
|
||||
process = container.exec(['bash', '/root/ovn-nb-cluster-join.sh'], timeout=5*60)
|
||||
out, warnings = process.wait_output()
|
||||
if warnings:
|
||||
for line in warnings.splitlines():
|
||||
logger.warning('CMD Out: %s', line.strip())
|
||||
|
||||
logging.debug("Attempting to join OVN_Southbound cluster")
|
||||
container = self.unit.get_container(OVN_SB_DB_CONTAINER)
|
||||
process = container.exec(['bash', '/root/ovn-sb-cluster-join.sh'], timeout=5*60)
|
||||
out, warnings = process.wait_output()
|
||||
if warnings:
|
||||
for line in warnings.splitlines():
|
||||
logger.warning('CMD Out: %s', line.strip())
|
||||
logging.debug("Starting services in DB containers")
|
||||
for ph in self.get_named_pebble_handlers([OVN_SB_DB_CONTAINER, OVN_NB_DB_CONTAINER]):
|
||||
ph.start_service()
|
||||
# Attempt to setup listers etc
|
||||
self.configure_ovn()
|
||||
self.unit.status = ops.model.ActiveStatus()
|
||||
|
||||
def configure_ovn(self):
|
||||
inactivity_probe = int(
|
||||
self.config['ovsdb-server-inactivity-probe']) * 1000
|
||||
self.configure_ovn_listener(
|
||||
'nb', {
|
||||
self.ovsdb_cms.db_nb_port: {
|
||||
'inactivity_probe': inactivity_probe,
|
||||
},
|
||||
})
|
||||
self.configure_ovn_listener(
|
||||
'sb', {
|
||||
self.ovsdb_cms.db_sb_port: {
|
||||
'role': 'ovn-controller',
|
||||
'inactivity_probe': inactivity_probe,
|
||||
},
|
||||
})
|
||||
self.configure_ovn_listener(
|
||||
'sb', {
|
||||
self.ovsdb_cms.db_sb_admin_port: {
|
||||
'inactivity_probe': inactivity_probe,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
class OVNCentralWallabyOperatorCharm(OVNCentralOperatorCharm):
|
||||
|
||||
openstack_release = 'wallaby'
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Note: use_juju_for_storage=True required per
|
||||
# https://github.com/canonical/operator/issues/506
|
||||
main(OVNCentralWallabyOperatorCharm, use_juju_for_storage=True)
|
236
charms/ovn-central-k8s/src/ovn.py
Normal file
236
charms/ovn-central-k8s/src/ovn.py
Normal file
@ -0,0 +1,236 @@
|
||||
# Copyright 2019 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 os
|
||||
import subprocess
|
||||
import uuid
|
||||
|
||||
import utils
|
||||
|
||||
|
||||
OVN_RUNDIR = '/var/run/ovn'
|
||||
OVN_SYSCONFDIR = '/etc/ovn'
|
||||
|
||||
|
||||
def ovn_appctl(target, args, rundir=None, use_ovs_appctl=False, cmd_executor=None):
|
||||
"""Run ovn/ovs-appctl for target with args and return output.
|
||||
|
||||
:param target: Name of daemon to contact. Unless target begins with '/',
|
||||
`ovn-appctl` looks for a pidfile and will build the path to
|
||||
a /var/run/ovn/target.pid.ctl for you.
|
||||
:type target: str
|
||||
:param args: Command and arguments to pass to `ovn-appctl`
|
||||
:type args: Tuple[str, ...]
|
||||
:param rundir: Override path to sockets
|
||||
:type rundir: Optional[str]
|
||||
:param use_ovs_appctl: The ``ovn-appctl`` command appeared in OVN 20.03,
|
||||
set this to True to use ``ovs-appctl`` instead.
|
||||
:type use_ovs_appctl: bool
|
||||
:returns: Output from command
|
||||
:rtype: str
|
||||
:raises: subprocess.CalledProcessError
|
||||
"""
|
||||
# NOTE(fnordahl): The ovsdb-server processes for the OVN databases use a
|
||||
# non-standard naming scheme for their daemon control socket and we need
|
||||
# to pass the full path to the socket.
|
||||
if target in ('ovnnb_db', 'ovnsb_db',):
|
||||
target = os.path.join(rundir or OVN_RUNDIR, target + '.ctl')
|
||||
|
||||
if use_ovs_appctl:
|
||||
tool = 'ovs-appctl'
|
||||
else:
|
||||
tool = 'ovn-appctl'
|
||||
|
||||
if not cmd_executor:
|
||||
cmd_executor = utils._run
|
||||
return cmd_executor(tool, '-t', target, *args)
|
||||
|
||||
|
||||
class OVNClusterStatus(object):
|
||||
|
||||
def __init__(self, name, cluster_id, server_id, address, status, role,
|
||||
term, leader, vote, election_timer, log,
|
||||
entries_not_yet_committed, entries_not_yet_applied,
|
||||
connections, servers):
|
||||
"""Initialize and populate OVNClusterStatus object.
|
||||
|
||||
Use class initializer so we can define types in a compatible manner.
|
||||
|
||||
:param name: Name of schema used for database
|
||||
:type name: str
|
||||
:param cluster_id: UUID of cluster
|
||||
:type cluster_id: uuid.UUID
|
||||
:param server_id: UUID of server
|
||||
:type server_id: uuid.UUID
|
||||
:param address: OVSDB connection method
|
||||
:type address: str
|
||||
:param status: Status text
|
||||
:type status: str
|
||||
:param role: Role of server
|
||||
:type role: str
|
||||
:param term: Election term
|
||||
:type term: int
|
||||
:param leader: Short form UUID of leader
|
||||
:type leader: str
|
||||
:param vote: Vote
|
||||
:type vote: str
|
||||
:param election_timer: Current value of election timer
|
||||
:type election_timer: int
|
||||
:param log: Log
|
||||
:type log: str
|
||||
:param entries_not_yet_committed: Entries not yet committed
|
||||
:type entries_not_yet_committed: int
|
||||
:param entries_not_yet_applied: Entries not yet applied
|
||||
:type entries_not_yet_applied: int
|
||||
:param connections: Connections
|
||||
:type connections: str
|
||||
:param servers: Servers in the cluster
|
||||
[('0ea6', 'ssl:192.0.2.42:6643')]
|
||||
:type servers: List[Tuple[str,str]]
|
||||
"""
|
||||
self.name = name
|
||||
self.cluster_id = cluster_id
|
||||
self.server_id = server_id
|
||||
self.address = address
|
||||
self.status = status
|
||||
self.role = role
|
||||
self.term = term
|
||||
self.leader = leader
|
||||
self.vote = vote
|
||||
self.election_timer = election_timer
|
||||
self.log = log
|
||||
self.entries_not_yet_committed = entries_not_yet_committed
|
||||
self.entries_not_yet_applied = entries_not_yet_applied
|
||||
self.connections = connections
|
||||
self.servers = servers
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
self.name == other.name and
|
||||
self.cluster_id == other.cluster_id and
|
||||
self.server_id == other.server_id and
|
||||
self.address == other.address and
|
||||
self.status == other.status and
|
||||
self.role == other.role and
|
||||
self.term == other.term and
|
||||
self.leader == other.leader and
|
||||
self.vote == other.vote and
|
||||
self.election_timer == other.election_timer and
|
||||
self.log == other.log and
|
||||
self.entries_not_yet_committed == other.entries_not_yet_committed and
|
||||
self.entries_not_yet_applied == other.entries_not_yet_applied and
|
||||
self.connections == other.connections and
|
||||
self.servers == other.servers)
|
||||
|
||||
@property
|
||||
def is_cluster_leader(self):
|
||||
"""Retrieve status information from clustered OVSDB.
|
||||
|
||||
:returns: Whether target is cluster leader
|
||||
:rtype: bool
|
||||
"""
|
||||
return self.leader == 'self'
|
||||
|
||||
|
||||
def cluster_status(target, schema=None, use_ovs_appctl=False, rundir=None, cmd_executor=None):
|
||||
"""Retrieve status information from clustered OVSDB.
|
||||
|
||||
:param target: Usually one of 'ovsdb-server', 'ovnnb_db', 'ovnsb_db', can
|
||||
also be full path to control socket.
|
||||
:type target: str
|
||||
:param schema: Database schema name, deduced from target if not provided
|
||||
:type schema: Optional[str]
|
||||
:param use_ovs_appctl: The ``ovn-appctl`` command appeared in OVN 20.03,
|
||||
set this to True to use ``ovs-appctl`` instead.
|
||||
:type use_ovs_appctl: bool
|
||||
:param rundir: Override path to sockets
|
||||
:type rundir: Optional[str]
|
||||
:returns: cluster status data object
|
||||
:rtype: OVNClusterStatus
|
||||
:raises: subprocess.CalledProcessError, KeyError, RuntimeError
|
||||
"""
|
||||
schema_map = {
|
||||
'ovnnb_db': 'OVN_Northbound',
|
||||
'ovnsb_db': 'OVN_Southbound',
|
||||
}
|
||||
if schema and schema not in schema_map.keys():
|
||||
raise RuntimeError('Unknown schema provided: "{}"'.format(schema))
|
||||
|
||||
status = {}
|
||||
k = ''
|
||||
for line in ovn_appctl(target,
|
||||
('cluster/status', schema or schema_map[target]),
|
||||
rundir=rundir,
|
||||
use_ovs_appctl=use_ovs_appctl,
|
||||
cmd_executor=cmd_executor).splitlines():
|
||||
if k and line.startswith(' '):
|
||||
# there is no key which means this is a instance of a multi-line/
|
||||
# multi-value item, populate the List which is already stored under
|
||||
# the key.
|
||||
if k == 'servers':
|
||||
status[k].append(
|
||||
tuple(line.replace(')', '').lstrip().split()[0:4:3]))
|
||||
else:
|
||||
status[k].append(line.lstrip())
|
||||
elif ':' in line:
|
||||
# this is a line with a key
|
||||
k, v = line.split(':', 1)
|
||||
k = k.lower()
|
||||
k = k.replace(' ', '_')
|
||||
if v:
|
||||
# this is a line with both key and value
|
||||
if k in ('cluster_id', 'server_id',):
|
||||
v = v.replace('(', '')
|
||||
v = v.replace(')', '')
|
||||
status[k] = tuple(v.split())
|
||||
else:
|
||||
status[k] = v.lstrip()
|
||||
else:
|
||||
# this is a line with only key which means a multi-line/
|
||||
# multi-value item. Store key as List which will be
|
||||
# populated on subsequent iterations.
|
||||
status[k] = []
|
||||
return OVNClusterStatus(
|
||||
status['name'],
|
||||
uuid.UUID(status['cluster_id'][1]),
|
||||
uuid.UUID(status['server_id'][1]),
|
||||
status['address'],
|
||||
status['status'],
|
||||
status['role'],
|
||||
int(status['term']),
|
||||
status['leader'],
|
||||
status['vote'],
|
||||
int(status['election_timer']),
|
||||
status['log'],
|
||||
int(status['entries_not_yet_committed']),
|
||||
int(status['entries_not_yet_applied']),
|
||||
status['connections'],
|
||||
status['servers'])
|
||||
|
||||
|
||||
def is_northd_active(cmd_executor=None):
|
||||
"""Query `ovn-northd` for active status.
|
||||
|
||||
Note that the active status information for ovn-northd is available for
|
||||
OVN 20.03 and onward.
|
||||
|
||||
:returns: True if local `ovn-northd` instance is active, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
try:
|
||||
for line in ovn_appctl('ovn-northd', ('status',), cmd_executor=cmd_executor).splitlines():
|
||||
if line.startswith('Status:') and 'active' in line:
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
return False
|
257
charms/ovn-central-k8s/src/ovsdb.py
Normal file
257
charms/ovn-central-k8s/src/ovsdb.py
Normal file
@ -0,0 +1,257 @@
|
||||
# Copyright 2019 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 json
|
||||
import uuid
|
||||
|
||||
import utils
|
||||
|
||||
|
||||
class SimpleOVSDB(object):
|
||||
"""Simple interface to OVSDB through the use of command line tools.
|
||||
|
||||
OVS and OVN is managed through a set of databases. These databases have
|
||||
similar command line tools to manage them. We make use of the similarity
|
||||
to provide a generic class that can be used to manage them.
|
||||
|
||||
The OpenvSwitch project does provide a Python API, but on the surface it
|
||||
appears to be a bit too involved for our simple use case.
|
||||
|
||||
Examples:
|
||||
sbdb = SimpleOVSDB('ovn-sbctl')
|
||||
for chs in sbdb.chassis:
|
||||
print(chs)
|
||||
|
||||
ovsdb = SimpleOVSDB('ovs-vsctl')
|
||||
for br in ovsdb.bridge:
|
||||
if br['name'] == 'br-test':
|
||||
ovsdb.bridge.set(br['uuid'], 'external_ids:charm', 'managed')
|
||||
|
||||
WARNING: If a list type field only have one item `ovs-vsctl` will present
|
||||
it as a single item. Since we do not know the schema we have no way of
|
||||
knowing what fields should be de-serialized as lists so the caller has
|
||||
to be careful of checking the type of values returned from this library.
|
||||
"""
|
||||
|
||||
# For validation we keep a complete map of currently known good tool and
|
||||
# table combinations. This requires maintenance down the line whenever
|
||||
# upstream adds things that downstream wants, and the cost of maintaining
|
||||
# that will most likely be lower then the cost of finding the needle in
|
||||
# the haystack whenever downstream code misspells something.
|
||||
_tool_table_map = {
|
||||
'ovs-vsctl': (
|
||||
'autoattach',
|
||||
'bridge',
|
||||
'ct_timeout_policy',
|
||||
'ct_zone',
|
||||
'controller',
|
||||
'datapath',
|
||||
'flow_sample_collector_set',
|
||||
'flow_table',
|
||||
'ipfix',
|
||||
'interface',
|
||||
'manager',
|
||||
'mirror',
|
||||
'netflow',
|
||||
'open_vswitch',
|
||||
'port',
|
||||
'qos',
|
||||
'queue',
|
||||
'ssl',
|
||||
'sflow',
|
||||
),
|
||||
'ovn-nbctl': (
|
||||
'acl',
|
||||
'address_set',
|
||||
'connection',
|
||||
'dhcp_options',
|
||||
'dns',
|
||||
'forwarding_group',
|
||||
'gateway_chassis',
|
||||
'ha_chassis',
|
||||
'ha_chassis_group',
|
||||
'load_balancer',
|
||||
'load_balancer_health_check',
|
||||
'logical_router',
|
||||
'logical_router_policy',
|
||||
'logical_router_port',
|
||||
'logical_router_static_route',
|
||||
'logical_switch',
|
||||
'logical_switch_port',
|
||||
'meter',
|
||||
'meter_band',
|
||||
'nat',
|
||||
'nb_global',
|
||||
'port_group',
|
||||
'qos',
|
||||
'ssl',
|
||||
),
|
||||
'ovn-sbctl': (
|
||||
'address_set',
|
||||
'chassis',
|
||||
'connection',
|
||||
'controller_event',
|
||||
'dhcp_options',
|
||||
'dhcpv6_options',
|
||||
'dns',
|
||||
'datapath_binding',
|
||||
'encap',
|
||||
'gateway_chassis',
|
||||
'ha_chassis',
|
||||
'ha_chassis_group',
|
||||
'igmp_group',
|
||||
'ip_multicast',
|
||||
'logical_flow',
|
||||
'mac_binding',
|
||||
'meter',
|
||||
'meter_band',
|
||||
'multicast_group',
|
||||
'port_binding',
|
||||
'port_group',
|
||||
'rbac_permission',
|
||||
'rbac_role',
|
||||
'sb_global',
|
||||
'ssl',
|
||||
'service_monitor',
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, tool, args=None, cmd_executor=None):
|
||||
"""SimpleOVSDB constructor.
|
||||
|
||||
:param tool: Which tool with database commands to operate on.
|
||||
Usually one of `ovs-vsctl`, `ovn-nbctl`, `ovn-sbctl`
|
||||
:type tool: str
|
||||
:param args: Extra arguments to pass to the tool
|
||||
:type args: Optional[List[str]]
|
||||
"""
|
||||
if tool not in self._tool_table_map:
|
||||
raise RuntimeError(
|
||||
'tool must be one of "{}"'.format(self._tool_table_map.keys()))
|
||||
self._tool = tool
|
||||
self._args = args
|
||||
self.cmd_executor = cmd_executor or utils._run
|
||||
|
||||
def __getattr__(self, table):
|
||||
if table not in self._tool_table_map[self._tool]:
|
||||
raise AttributeError(
|
||||
'table "{}" not known for use with "{}"'
|
||||
.format(table, self._tool))
|
||||
return self.Table(self._tool, table, args=self._args, cmd_executor=self.cmd_executor)
|
||||
|
||||
class Table(object):
|
||||
"""Methods to interact with contents of OVSDB tables.
|
||||
|
||||
NOTE: At the time of this writing ``find`` is the only command
|
||||
line argument to OVSDB manipulating tools that actually supports
|
||||
JSON output.
|
||||
"""
|
||||
|
||||
def __init__(self, tool, table, args=None, cmd_executor=None):
|
||||
"""SimpleOVSDBTable constructor.
|
||||
|
||||
:param table: Which table to operate on
|
||||
:type table: str
|
||||
:param args: Extra arguments to pass to the tool
|
||||
:type args: Optional[List[str]]
|
||||
"""
|
||||
self._tool = tool
|
||||
self._table = table
|
||||
self._args = args
|
||||
self.cmd_executor = cmd_executor or utils._run
|
||||
|
||||
def _deserialize_ovsdb(self, data):
|
||||
"""Deserialize OVSDB RFC7047 section 5.1 data.
|
||||
|
||||
:param data: Multidimensional list where first row contains RFC7047
|
||||
type information
|
||||
:type data: List[str,any]
|
||||
:returns: Deserialized data.
|
||||
:rtype: any
|
||||
"""
|
||||
# When using json formatted output to OVS commands Internal OVSDB
|
||||
# notation may occur that require further deserializing.
|
||||
# Reference: https://tools.ietf.org/html/rfc7047#section-5.1
|
||||
ovs_type_cb_map = {
|
||||
'uuid': uuid.UUID,
|
||||
# NOTE: OVSDB sets have overloaded type
|
||||
# see special handling below
|
||||
'set': list,
|
||||
'map': dict,
|
||||
}
|
||||
assert len(data) > 1, ('Invalid data provided, expecting list '
|
||||
'with at least two elements.')
|
||||
if data[0] == 'set':
|
||||
# special handling for set
|
||||
#
|
||||
# it is either a list of strings or a list of typed lists.
|
||||
# taste first element to see which it is
|
||||
for el in data[1]:
|
||||
# NOTE: We lock this handling down to the `uuid` type as
|
||||
# that is the only one we have a practical example of.
|
||||
# We could potentially just handle this generally based on
|
||||
# the types listed in `ovs_type_cb_map` but let's open for
|
||||
# that as soon as we have a concrete example to validate on
|
||||
if isinstance(
|
||||
el, list) and len(el) and el[0] == 'uuid':
|
||||
decoded_set = []
|
||||
for el in data[1]:
|
||||
decoded_set.append(self._deserialize_ovsdb(el))
|
||||
return(decoded_set)
|
||||
# fall back to normal processing below
|
||||
break
|
||||
|
||||
# Use map to deserialize data with fallback to `str`
|
||||
f = ovs_type_cb_map.get(data[0], str)
|
||||
return f(data[1])
|
||||
|
||||
def _find_tbl(self, condition=None):
|
||||
"""Run and parse output of OVSDB `find` command.
|
||||
|
||||
:param condition: An optional RFC 7047 5.1 match condition
|
||||
:type condition: Optional[str]
|
||||
:returns: Dictionary with data
|
||||
:rtype: Dict[str, any]
|
||||
"""
|
||||
cmd = [self._tool]
|
||||
if self._args:
|
||||
cmd.extend(self._args)
|
||||
cmd.extend(['-f', 'json', 'find', self._table])
|
||||
if condition:
|
||||
cmd.append(condition)
|
||||
output = self.cmd_executor(*cmd)
|
||||
data = json.loads(output)
|
||||
for row in data['data']:
|
||||
values = []
|
||||
for col in row:
|
||||
if isinstance(col, list) and len(col) > 1:
|
||||
values.append(self._deserialize_ovsdb(col))
|
||||
else:
|
||||
values.append(col)
|
||||
yield dict(zip(data['headings'], values))
|
||||
|
||||
def __iter__(self):
|
||||
return self._find_tbl()
|
||||
|
||||
def clear(self, rec, col):
|
||||
self.cmd_executor(self._tool, 'clear', self._table, rec, col)
|
||||
|
||||
def find(self, condition):
|
||||
return self._find_tbl(condition=condition)
|
||||
|
||||
def remove(self, rec, col, value):
|
||||
self.cmd_executor(self._tool, 'remove', self._table, rec, col, value)
|
||||
|
||||
def set(self, rec, col, value):
|
||||
self.cmd_executor(self._tool, 'set', self._table, rec,
|
||||
'{}={}'.format(col, value))
|
2
charms/ovn-central-k8s/src/templates/cert_host.j2
Normal file
2
charms/ovn-central-k8s/src/templates/cert_host.j2
Normal file
@ -0,0 +1,2 @@
|
||||
# {{ certificates }}
|
||||
{{ certificates.cert }}
|
1
charms/ovn-central-k8s/src/templates/key_host.j2
Normal file
1
charms/ovn-central-k8s/src/templates/key_host.j2
Normal file
@ -0,0 +1 @@
|
||||
{{ certificates.key }}
|
1
charms/ovn-central-k8s/src/templates/ovn-central.crt.j2
Normal file
1
charms/ovn-central-k8s/src/templates/ovn-central.crt.j2
Normal file
@ -0,0 +1 @@
|
||||
{{ certificates.ca_cert }}
|
@ -0,0 +1,16 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
WRAPPER_LOG=/var/log/ovn/cluster_join_wrapper.log
|
||||
|
||||
function log_msg()
|
||||
{
|
||||
msg=$1
|
||||
echo "$(date): $msg" >> $WRAPPER_LOG
|
||||
}
|
||||
|
||||
ls -l /var/lib/ovn/{.ovn,ovn}* &> /dev/null && { log_msg "Existing DB files found, skipping join"; exit 0; }
|
||||
log_msg "No existing DB files present"
|
||||
ADDRESSES="{{ peers.db_nb_cluster_connection_strs|join(' ') }}"
|
||||
log_msg "Running join-cluster cmd: ovsdb-tool join-cluster /var/lib/ovn/ovnnb_db.db OVN_Northbound $ADDRESSES"
|
||||
ovsdb-tool join-cluster /var/lib/ovn/ovnnb_db.db OVN_Northbound $ADDRESSES &> $WRAPPER_LOG
|
||||
|
@ -0,0 +1,31 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
WRAPPER_LOG=/var/log/ovn/service_wrapper.log
|
||||
|
||||
function log_msg()
|
||||
{
|
||||
msg=$1
|
||||
echo "$(date): $msg" >> $WRAPPER_LOG
|
||||
}
|
||||
|
||||
function stop_svc()
|
||||
{
|
||||
log_msg "Stopping"
|
||||
/usr/share/ovn/scripts/ovn-ctl stop_nb_ovsdb 2>&1
|
||||
}
|
||||
|
||||
trap stop_svc EXIT
|
||||
|
||||
log_msg "Starting"
|
||||
|
||||
/usr/share/ovn/scripts/ovn-ctl run_nb_ovsdb \
|
||||
--db-nb-cluster-local-addr={{ peers.cluster_local_addr }} \
|
||||
--db-nb-cluster-local-port={{ peers.db_nb_cluster_port }} \
|
||||
--db-nb-cluster-local-proto=ssl \
|
||||
--db-nb-cluster-remote-addr={{ peers.cluster_remote_addrs | first if not ovs_db.is_charm_leader else '' }} \
|
||||
--db-nb-cluster-remote-port={{ peers.db_nb_cluster_port }} \
|
||||
--db-nb-cluster-remote-proto=ssl \
|
||||
--ovn-nb-db-ssl-key={{ ovs_db.ovn_key }} \
|
||||
--ovn-nb-db-ssl-cert={{ ovs_db.ovn_cert }} \
|
||||
--ovn-nb-db-ssl-ca-cert={{ ovs_db.ovn_ca_cert }} \
|
||||
--ovn-nb-logfile=/var/log/ovn/ovn-nb-db.log
|
@ -0,0 +1,5 @@
|
||||
--ovnnb-db={{ peers.db_nb_connection_strs|join(',') }}
|
||||
--ovnsb-db={{ peers.db_sb_connection_strs|join(',') }}
|
||||
-c {{ ovs_db.ovn_cert }}
|
||||
-C {{ ovs_db.ovn_ca_cert }}
|
||||
-p {{ ovs_db.ovn_key }}
|
@ -0,0 +1,36 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
WRAPPER_LOG=/var/log/ovn/service_wrapper.log
|
||||
|
||||
function log_msg()
|
||||
{
|
||||
msg=$1
|
||||
echo "$(date): $msg" >> $WRAPPER_LOG
|
||||
}
|
||||
|
||||
function stop_svc()
|
||||
{
|
||||
log_msg "Stopping"
|
||||
/usr/share/ovn/scripts/ovn-ctl stop_northd 2>&1
|
||||
}
|
||||
|
||||
trap stop_svc EXIT
|
||||
|
||||
log_msg "Starting"
|
||||
|
||||
# Switch to using ovn-ctl if its possible to stop --detach
|
||||
# being set
|
||||
|
||||
ovn-northd \
|
||||
-vconsole:emer \
|
||||
-vsyslog:err \
|
||||
-vfile:info \
|
||||
--ovnnb-db={{ peers.db_nb_connection_strs|join(',') }} \
|
||||
--ovnsb-db={{ peers.db_sb_connection_strs|join(',') }} \
|
||||
-c {{ ovs_db.ovn_cert }} \
|
||||
-C {{ ovs_db.ovn_ca_cert }} \
|
||||
-p {{ ovs_db.ovn_key }} \
|
||||
--no-chdir \
|
||||
--log-file=/var/log/ovn/ovn-northd.log \
|
||||
--pidfile=/var/run/ovn/ovn-northd.pid
|
||||
|
@ -0,0 +1,15 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
WRAPPER_LOG=/var/log/ovn/cluster_join_wrapper.log
|
||||
|
||||
function log_msg()
|
||||
{
|
||||
msg=$1
|
||||
echo "$(date): $msg" >> $WRAPPER_LOG
|
||||
}
|
||||
ls -l /var/lib/ovn/{.ovn,ovn}* &> /dev/null && { log_msg "Existing DB files found, skipping join"; exit 0; }
|
||||
log_msg "No existing DB files present"
|
||||
ADDRESSES="{{ peers.db_sb_cluster_connection_strs|join(' ') }}"
|
||||
log_msg "Running join-cluster cmd: ovsdb-tool join-cluster /var/lib/ovn/ovnsb_db.db OVN_Southbound $ADDRESSES"
|
||||
# ovsdb-tool join-cluster /var/lib/ovn/ovnsb_db.db OVN_Southbound ssl\:10.1.8.116\:6644 ssl\:10.1.46.82\:6644 ssl\:10.1.41.7\:6644
|
||||
ovsdb-tool join-cluster /var/lib/ovn/ovnsb_db.db OVN_Southbound $ADDRESSES &> $WRAPPER_LOG
|
@ -0,0 +1,31 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
WRAPPER_LOG=/var/log/ovn/service_wrapper.log
|
||||
|
||||
function log_msg()
|
||||
{
|
||||
msg=$1
|
||||
echo "$(date): $msg" >> $WRAPPER_LOG
|
||||
}
|
||||
|
||||
function stop_svc()
|
||||
{
|
||||
log_msg "Stopping"
|
||||
/usr/share/ovn/scripts/ovn-ctl stop_sb_ovsdb 2>&1
|
||||
}
|
||||
|
||||
trap stop_svc EXIT
|
||||
|
||||
log_msg "Starting"
|
||||
|
||||
/usr/share/ovn/scripts/ovn-ctl run_sb_ovsdb \
|
||||
--db-sb-cluster-local-addr={{ peers.cluster_local_addr }} \
|
||||
--db-sb-cluster-local-port={{ peers.db_sb_cluster_port }} \
|
||||
--db-sb-cluster-local-proto=ssl \
|
||||
--db-sb-cluster-remote-addr={{ peers.cluster_remote_addrs | first if not ovs_db.is_charm_leader else '' }} \
|
||||
--db-sb-cluster-remote-port={{ peers.db_sb_cluster_port }} \
|
||||
--db-sb-cluster-remote-proto=ssl \
|
||||
--ovn-sb-db-ssl-key={{ ovs_db.ovn_key }} \
|
||||
--ovn-sb-db-ssl-cert={{ ovs_db.ovn_cert }} \
|
||||
--ovn-sb-db-ssl-ca-cert={{ ovs_db.ovn_ca_cert }} \
|
||||
--ovn-sb-logfile=/var/log/ovn/ovn-sb-db.log
|
26
charms/ovn-central-k8s/src/utils.py
Normal file
26
charms/ovn-central-k8s/src/utils.py
Normal file
@ -0,0 +1,26 @@
|
||||
# Copyright 2019 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 subprocess
|
||||
|
||||
|
||||
def _run(*args):
|
||||
"""Run a process, check result, capture decoded output from STDOUT.
|
||||
|
||||
:param args: Command and arguments to run
|
||||
:type args: Tuple[str, ...]
|
||||
:returns: Information about the completed process
|
||||
:rtype: str
|
||||
:raises subprocess.CalledProcessError
|
||||
"""
|
||||
return subprocess.check_output(args, universal_newlines=True)
|
17
charms/ovn-central-k8s/test-requirements.txt
Normal file
17
charms/ovn-central-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?)
|
0
charms/ovn-central-k8s/tests/__init__.py
Normal file
0
charms/ovn-central-k8s/tests/__init__.py
Normal file
66
charms/ovn-central-k8s/tests/test_charm.py
Normal file
66
charms/ovn-central-k8s/tests/test_charm.py
Normal file
@ -0,0 +1,66 @@
|
||||
# Copyright 2022 liam
|
||||
# See LICENSE file for licensing details.
|
||||
#
|
||||
# Learn more about testing at: https://juju.is/docs/sdk/testing
|
||||
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
|
||||
from charm import SunbeamOvnCentralOperatorCharm
|
||||
from ops.model import ActiveStatus
|
||||
from ops.testing import Harness
|
||||
|
||||
|
||||
class TestCharm(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.harness = Harness(SunbeamOvnCentralOperatorCharm)
|
||||
self.addCleanup(self.harness.cleanup)
|
||||
self.harness.begin()
|
||||
|
||||
def test_config_changed(self):
|
||||
self.assertEqual(list(self.harness.charm._stored.things), [])
|
||||
self.harness.update_config({"thing": "foo"})
|
||||
self.assertEqual(list(self.harness.charm._stored.things), ["foo"])
|
||||
|
||||
def test_action(self):
|
||||
# the harness doesn't (yet!) help much with actions themselves
|
||||
action_event = Mock(params={"fail": ""})
|
||||
self.harness.charm._on_fortune_action(action_event)
|
||||
|
||||
self.assertTrue(action_event.set_results.called)
|
||||
|
||||
def test_action_fail(self):
|
||||
action_event = Mock(params={"fail": "fail this"})
|
||||
self.harness.charm._on_fortune_action(action_event)
|
||||
|
||||
self.assertEqual(action_event.fail.call_args, [("fail this",)])
|
||||
|
||||
def test_httpbin_pebble_ready(self):
|
||||
# Check the initial Pebble plan is empty
|
||||
initial_plan = self.harness.get_container_pebble_plan("httpbin")
|
||||
self.assertEqual(initial_plan.to_yaml(), "{}\n")
|
||||
# Expected plan after Pebble ready with default config
|
||||
expected_plan = {
|
||||
"services": {
|
||||
"httpbin": {
|
||||
"override": "replace",
|
||||
"summary": "httpbin",
|
||||
"command": "gunicorn -b 0.0.0.0:80 httpbin:app -k gevent",
|
||||
"startup": "enabled",
|
||||
"environment": {"thing": "🎁"},
|
||||
}
|
||||
},
|
||||
}
|
||||
# Get the httpbin container from the model
|
||||
container = self.harness.model.unit.get_container("httpbin")
|
||||
# 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()
|
||||
# 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")
|
||||
self.assertTrue(service.is_running())
|
||||
# Ensure we set an ActiveStatus with no message
|
||||
self.assertEqual(self.harness.model.unit.status, ActiveStatus())
|
134
charms/ovn-central-k8s/tox.ini
Normal file
134
charms/ovn-central-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
|
Loading…
x
Reference in New Issue
Block a user