2 # Copyright 2021 Canonical Ltd.
4 # Licensed under the Apache License, Version 2.0 (the "License"); you may
5 # not use this file except in compliance with the License. You may obtain
6 # a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 # License for the specific language governing permissions and limitations
16 # For those usages not covered by the Apache License, Version 2.0 please
17 # contact: legal@canonical.com
19 # To get in touch with the maintainers, please contact:
20 # osm-charmers@lists.launchpad.net
23 # pylint: disable=E0213
26 from ipaddress
import ip_network
28 from typing
import NoReturn
, Optional
29 from urllib
.parse
import urlparse
32 from ops
.main
import main
33 from opslib
.osm
.charm
import CharmedOsmBase
, RelationsMissing
34 from opslib
.osm
.interfaces
.http
import HttpServer
35 from opslib
.osm
.interfaces
.kafka
import KafkaClient
36 from opslib
.osm
.interfaces
.keystone
import KeystoneClient
37 from opslib
.osm
.interfaces
.mongo
import MongoClient
38 from opslib
.osm
.interfaces
.prometheus
import PrometheusClient
39 from opslib
.osm
.pod
import (
41 IngressResourceV3Builder
,
44 from opslib
.osm
.validator
import ModelValidator
, validator
47 logger
= logging
.getLogger(__name__
)
52 class ConfigModel(ModelValidator
):
55 database_commonkey
: str
58 site_url
: Optional
[str]
59 cluster_issuer
: Optional
[str]
60 ingress_class
: Optional
[str]
61 ingress_whitelist_source_range
: Optional
[str]
62 tls_secret_name
: Optional
[str]
63 mongodb_uri
: Optional
[str]
65 @validator("auth_backend")
66 def validate_auth_backend(cls
, v
):
67 if v
not in {"internal", "keystone"}:
68 raise ValueError("value must be 'internal' or 'keystone'")
71 @validator("log_level")
72 def validate_log_level(cls
, v
):
73 if v
not in {"INFO", "DEBUG"}:
74 raise ValueError("value must be INFO or DEBUG")
77 @validator("max_file_size")
78 def validate_max_file_size(cls
, v
):
80 raise ValueError("value must be equal or greater than 0")
83 @validator("site_url")
84 def validate_site_url(cls
, v
):
87 if not parsed
.scheme
.startswith("http"):
88 raise ValueError("value must start with http")
91 @validator("ingress_whitelist_source_range")
92 def validate_ingress_whitelist_source_range(cls
, v
):
97 @validator("mongodb_uri")
98 def validate_mongodb_uri(cls
, v
):
99 if v
and not v
.startswith("mongodb://"):
100 raise ValueError("mongodb_uri is not properly formed")
104 class NbiCharm(CharmedOsmBase
):
105 def __init__(self
, *args
) -> NoReturn
:
106 super().__init
__(*args
, oci_image
="image")
108 self
.kafka_client
= KafkaClient(self
, "kafka")
109 self
.framework
.observe(self
.on
["kafka"].relation_changed
, self
.configure_pod
)
110 self
.framework
.observe(self
.on
["kafka"].relation_broken
, self
.configure_pod
)
112 self
.mongodb_client
= MongoClient(self
, "mongodb")
113 self
.framework
.observe(self
.on
["mongodb"].relation_changed
, self
.configure_pod
)
114 self
.framework
.observe(self
.on
["mongodb"].relation_broken
, self
.configure_pod
)
116 self
.prometheus_client
= PrometheusClient(self
, "prometheus")
117 self
.framework
.observe(
118 self
.on
["prometheus"].relation_changed
, self
.configure_pod
120 self
.framework
.observe(
121 self
.on
["prometheus"].relation_broken
, self
.configure_pod
124 self
.keystone_client
= KeystoneClient(self
, "keystone")
125 self
.framework
.observe(self
.on
["keystone"].relation_changed
, self
.configure_pod
)
126 self
.framework
.observe(self
.on
["keystone"].relation_broken
, self
.configure_pod
)
128 self
.http_server
= HttpServer(self
, "nbi")
129 self
.framework
.observe(self
.on
["nbi"].relation_joined
, self
._publish
_nbi
_info
)
131 def _publish_nbi_info(self
, event
):
132 """Publishes NBI information.
135 event (EventBase): RO relation event.
137 if self
.unit
.is_leader():
138 self
.http_server
.publish_info(self
.app
.name
, PORT
)
140 def _check_missing_dependencies(self
, config
: ConfigModel
):
141 missing_relations
= []
143 if self
.kafka_client
.is_missing_data_in_unit():
144 missing_relations
.append("kafka")
145 if not config
.mongodb_uri
and self
.mongodb_client
.is_missing_data_in_unit():
146 missing_relations
.append("mongodb")
147 if self
.prometheus_client
.is_missing_data_in_app():
148 missing_relations
.append("prometheus")
149 if config
.auth_backend
== "keystone":
150 if self
.keystone_client
.is_missing_data_in_app():
151 missing_relations
.append("keystone")
153 if missing_relations
:
154 raise RelationsMissing(missing_relations
)
156 def build_pod_spec(self
, image_info
):
158 config
= ConfigModel(**dict(self
.config
))
160 if config
.mongodb_uri
and not self
.mongodb_client
.is_missing_data_in_unit():
161 raise Exception("Mongodb data cannot be provided via config and relation")
164 self
._check
_missing
_dependencies
(config
)
166 # Create Builder for the PodSpec
167 pod_spec_builder
= PodSpecV3Builder()
169 # Build Init Container
170 pod_spec_builder
.add_init_container(
172 "name": "init-check",
173 "image": "alpine:latest",
177 f
"until (nc -zvw1 {self.kafka_client.host} {self.kafka_client.port} ); do sleep 3; done; exit 0",
183 container_builder
= ContainerV3Builder(self
.app
.name
, image_info
)
184 container_builder
.add_port(name
=self
.app
.name
, port
=PORT
)
185 container_builder
.add_tcpsocket_readiness_probe(
187 initial_delay_seconds
=5,
190 container_builder
.add_tcpsocket_liveness_probe(
192 initial_delay_seconds
=45,
195 container_builder
.add_envs(
197 # General configuration
198 "ALLOW_ANONYMOUS_LOGIN": "yes",
199 "OSMNBI_SERVER_ENABLE_TEST": config
.enable_test
,
200 "OSMNBI_STATIC_DIR": "/app/osm_nbi/html_public",
201 # Kafka configuration
202 "OSMNBI_MESSAGE_HOST": self
.kafka_client
.host
,
203 "OSMNBI_MESSAGE_DRIVER": "kafka",
204 "OSMNBI_MESSAGE_PORT": self
.kafka_client
.port
,
205 # Database configuration
206 "OSMNBI_DATABASE_DRIVER": "mongo",
207 "OSMNBI_DATABASE_URI": config
.mongodb_uri
208 or self
.mongodb_client
.connection_string
,
209 "OSMNBI_DATABASE_COMMONKEY": config
.database_commonkey
,
210 # Storage configuration
211 "OSMNBI_STORAGE_DRIVER": "mongo",
212 "OSMNBI_STORAGE_PATH": "/app/storage",
213 "OSMNBI_STORAGE_COLLECTION": "files",
214 "OSMNBI_STORAGE_URI": config
.mongodb_uri
215 or self
.mongodb_client
.connection_string
,
216 # Prometheus configuration
217 "OSMNBI_PROMETHEUS_HOST": self
.prometheus_client
.hostname
,
218 "OSMNBI_PROMETHEUS_PORT": self
.prometheus_client
.port
,
220 "OSMNBI_LOG_LEVEL": config
.log_level
,
223 if config
.auth_backend
== "internal":
224 container_builder
.add_env("OSMNBI_AUTHENTICATION_BACKEND", "internal")
225 elif config
.auth_backend
== "keystone":
226 container_builder
.add_envs(
228 "OSMNBI_AUTHENTICATION_BACKEND": "keystone",
229 "OSMNBI_AUTHENTICATION_AUTH_URL": self
.keystone_client
.host
,
230 "OSMNBI_AUTHENTICATION_AUTH_PORT": self
.keystone_client
.port
,
231 "OSMNBI_AUTHENTICATION_USER_DOMAIN_NAME": self
.keystone_client
.user_domain_name
,
232 "OSMNBI_AUTHENTICATION_PROJECT_DOMAIN_NAME": self
.keystone_client
.project_domain_name
,
233 "OSMNBI_AUTHENTICATION_SERVICE_USERNAME": self
.keystone_client
.username
,
234 "OSMNBI_AUTHENTICATION_SERVICE_PASSWORD": self
.keystone_client
.password
,
235 "OSMNBI_AUTHENTICATION_SERVICE_PROJECT": self
.keystone_client
.service
,
238 container
= container_builder
.build()
240 # Add container to pod spec
241 pod_spec_builder
.add_container(container
)
243 # Add ingress resources to pod spec if site url exists
245 parsed
= urlparse(config
.site_url
)
247 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
248 str(config
.max_file_size
) + "m"
249 if config
.max_file_size
> 0
250 else config
.max_file_size
252 "nginx.ingress.kubernetes.io/backend-protocol": "HTTPS",
254 if config
.ingress_class
:
255 annotations
["kubernetes.io/ingress.class"] = config
.ingress_class
256 ingress_resource_builder
= IngressResourceV3Builder(
257 f
"{self.app.name}-ingress", annotations
260 if config
.ingress_whitelist_source_range
:
262 "nginx.ingress.kubernetes.io/whitelist-source-range"
263 ] = config
.ingress_whitelist_source_range
265 if config
.cluster_issuer
:
266 annotations
["cert-manager.io/cluster-issuer"] = config
.cluster_issuer
268 if parsed
.scheme
== "https":
269 ingress_resource_builder
.add_tls(
270 [parsed
.hostname
], config
.tls_secret_name
273 annotations
["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
275 ingress_resource_builder
.add_rule(parsed
.hostname
, self
.app
.name
, PORT
)
276 ingress_resource
= ingress_resource_builder
.build()
277 pod_spec_builder
.add_ingress_resource(ingress_resource
)
279 logger
.debug(pod_spec_builder
.build())
281 return pod_spec_builder
.build()
284 if __name__
== "__main__":