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
,
45 from opslib
.osm
.validator
import ModelValidator
, validator
48 logger
= logging
.getLogger(__name__
)
53 class ConfigModel(ModelValidator
):
56 database_commonkey
: str
59 site_url
: Optional
[str]
60 cluster_issuer
: Optional
[str]
61 ingress_class
: Optional
[str]
62 ingress_whitelist_source_range
: Optional
[str]
63 tls_secret_name
: Optional
[str]
64 mongodb_uri
: Optional
[str]
65 image_pull_policy
: str
67 security_context
: bool
69 @validator("auth_backend")
70 def validate_auth_backend(cls
, v
):
71 if v
not in {"internal", "keystone"}:
72 raise ValueError("value must be 'internal' or 'keystone'")
75 @validator("log_level")
76 def validate_log_level(cls
, v
):
77 if v
not in {"INFO", "DEBUG"}:
78 raise ValueError("value must be INFO or DEBUG")
81 @validator("max_file_size")
82 def validate_max_file_size(cls
, v
):
84 raise ValueError("value must be equal or greater than 0")
87 @validator("site_url")
88 def validate_site_url(cls
, v
):
91 if not parsed
.scheme
.startswith("http"):
92 raise ValueError("value must start with http")
95 @validator("ingress_whitelist_source_range")
96 def validate_ingress_whitelist_source_range(cls
, v
):
101 @validator("mongodb_uri")
102 def validate_mongodb_uri(cls
, v
):
103 if v
and not v
.startswith("mongodb://"):
104 raise ValueError("mongodb_uri is not properly formed")
107 @validator("image_pull_policy")
108 def validate_image_pull_policy(cls
, v
):
111 "ifnotpresent": "IfNotPresent",
115 if v
not in values
.keys():
116 raise ValueError("value must be always, ifnotpresent or never")
120 class NbiCharm(CharmedOsmBase
):
121 def __init__(self
, *args
) -> NoReturn
:
125 debug_mode_config_key
="debug_mode",
126 debug_pubkey_config_key
="debug_pubkey",
127 vscode_workspace
=VSCODE_WORKSPACE
,
130 self
.kafka_client
= KafkaClient(self
, "kafka")
131 self
.framework
.observe(self
.on
["kafka"].relation_changed
, self
.configure_pod
)
132 self
.framework
.observe(self
.on
["kafka"].relation_broken
, self
.configure_pod
)
134 self
.mongodb_client
= MongoClient(self
, "mongodb")
135 self
.framework
.observe(self
.on
["mongodb"].relation_changed
, self
.configure_pod
)
136 self
.framework
.observe(self
.on
["mongodb"].relation_broken
, self
.configure_pod
)
138 self
.prometheus_client
= PrometheusClient(self
, "prometheus")
139 self
.framework
.observe(
140 self
.on
["prometheus"].relation_changed
, self
.configure_pod
142 self
.framework
.observe(
143 self
.on
["prometheus"].relation_broken
, self
.configure_pod
146 self
.keystone_client
= KeystoneClient(self
, "keystone")
147 self
.framework
.observe(self
.on
["keystone"].relation_changed
, self
.configure_pod
)
148 self
.framework
.observe(self
.on
["keystone"].relation_broken
, self
.configure_pod
)
150 self
.http_server
= HttpServer(self
, "nbi")
151 self
.framework
.observe(self
.on
["nbi"].relation_joined
, self
._publish
_nbi
_info
)
153 def _publish_nbi_info(self
, event
):
154 """Publishes NBI information.
157 event (EventBase): RO relation event.
159 if self
.unit
.is_leader():
160 self
.http_server
.publish_info(self
.app
.name
, PORT
)
162 def _check_missing_dependencies(self
, config
: ConfigModel
):
163 missing_relations
= []
166 self
.kafka_client
.is_missing_data_in_unit()
167 and self
.kafka_client
.is_missing_data_in_app()
169 missing_relations
.append("kafka")
170 if not config
.mongodb_uri
and self
.mongodb_client
.is_missing_data_in_unit():
171 missing_relations
.append("mongodb")
172 if self
.prometheus_client
.is_missing_data_in_app():
173 missing_relations
.append("prometheus")
174 if config
.auth_backend
== "keystone":
175 if self
.keystone_client
.is_missing_data_in_app():
176 missing_relations
.append("keystone")
178 if missing_relations
:
179 raise RelationsMissing(missing_relations
)
181 def build_pod_spec(self
, image_info
):
183 config
= ConfigModel(**dict(self
.config
))
185 if config
.mongodb_uri
and not self
.mongodb_client
.is_missing_data_in_unit():
186 raise Exception("Mongodb data cannot be provided via config and relation")
189 self
._check
_missing
_dependencies
(config
)
191 security_context_enabled
= (
192 config
.security_context
if not config
.debug_mode
else False
195 # Create Builder for the PodSpec
196 pod_spec_builder
= PodSpecV3Builder(
197 enable_security_context
=security_context_enabled
200 # Add secrets to the pod
201 mongodb_secret_name
= f
"{self.app.name}-mongodb-secret"
202 pod_spec_builder
.add_secret(
205 "uri": config
.mongodb_uri
or self
.mongodb_client
.connection_string
,
206 "commonkey": config
.database_commonkey
,
210 # Build Init Container
211 pod_spec_builder
.add_init_container(
213 "name": "init-check",
214 "image": "alpine:latest",
218 f
"until (nc -zvw1 {self.kafka_client.host} {self.kafka_client.port} ); do sleep 3; done; exit 0",
224 container_builder
= ContainerV3Builder(
227 config
.image_pull_policy
,
228 run_as_non_root
=security_context_enabled
,
230 container_builder
.add_port(name
=self
.app
.name
, port
=PORT
)
231 container_builder
.add_tcpsocket_readiness_probe(
233 initial_delay_seconds
=5,
236 container_builder
.add_tcpsocket_liveness_probe(
238 initial_delay_seconds
=45,
241 container_builder
.add_envs(
243 # General configuration
244 "ALLOW_ANONYMOUS_LOGIN": "yes",
245 "OSMNBI_SERVER_ENABLE_TEST": config
.enable_test
,
246 "OSMNBI_STATIC_DIR": "/app/osm_nbi/html_public",
247 # Kafka configuration
248 "OSMNBI_MESSAGE_HOST": self
.kafka_client
.host
,
249 "OSMNBI_MESSAGE_DRIVER": "kafka",
250 "OSMNBI_MESSAGE_PORT": self
.kafka_client
.port
,
251 # Database configuration
252 "OSMNBI_DATABASE_DRIVER": "mongo",
253 # Storage configuration
254 "OSMNBI_STORAGE_DRIVER": "mongo",
255 "OSMNBI_STORAGE_PATH": "/app/storage",
256 "OSMNBI_STORAGE_COLLECTION": "files",
257 # Prometheus configuration
258 "OSMNBI_PROMETHEUS_HOST": self
.prometheus_client
.hostname
,
259 "OSMNBI_PROMETHEUS_PORT": self
.prometheus_client
.port
,
261 "OSMNBI_LOG_LEVEL": config
.log_level
,
264 container_builder
.add_secret_envs(
265 secret_name
=mongodb_secret_name
,
267 "OSMNBI_DATABASE_URI": "uri",
268 "OSMNBI_DATABASE_COMMONKEY": "commonkey",
269 "OSMNBI_STORAGE_URI": "uri",
272 if config
.auth_backend
== "internal":
273 container_builder
.add_env("OSMNBI_AUTHENTICATION_BACKEND", "internal")
274 elif config
.auth_backend
== "keystone":
275 keystone_secret_name
= f
"{self.app.name}-keystone-secret"
276 pod_spec_builder
.add_secret(
277 keystone_secret_name
,
279 "url": self
.keystone_client
.host
,
280 "port": self
.keystone_client
.port
,
281 "user_domain": self
.keystone_client
.user_domain_name
,
282 "project_domain": self
.keystone_client
.project_domain_name
,
283 "service_username": self
.keystone_client
.username
,
284 "service_password": self
.keystone_client
.password
,
285 "service_project": self
.keystone_client
.service
,
288 container_builder
.add_env("OSMNBI_AUTHENTICATION_BACKEND", "keystone")
289 container_builder
.add_secret_envs(
290 secret_name
=keystone_secret_name
,
292 "OSMNBI_AUTHENTICATION_AUTH_URL": "url",
293 "OSMNBI_AUTHENTICATION_AUTH_PORT": "port",
294 "OSMNBI_AUTHENTICATION_USER_DOMAIN_NAME": "user_domain",
295 "OSMNBI_AUTHENTICATION_PROJECT_DOMAIN_NAME": "project_domain",
296 "OSMNBI_AUTHENTICATION_SERVICE_USERNAME": "service_username",
297 "OSMNBI_AUTHENTICATION_SERVICE_PASSWORD": "service_password",
298 "OSMNBI_AUTHENTICATION_SERVICE_PROJECT": "service_project",
301 container
= container_builder
.build()
303 # Add container to pod spec
304 pod_spec_builder
.add_container(container
)
306 # Add ingress resources to pod spec if site url exists
308 parsed
= urlparse(config
.site_url
)
310 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
311 str(config
.max_file_size
) + "m"
312 if config
.max_file_size
> 0
313 else config
.max_file_size
315 "nginx.ingress.kubernetes.io/backend-protocol": "HTTPS",
317 if config
.ingress_class
:
318 annotations
["kubernetes.io/ingress.class"] = config
.ingress_class
319 ingress_resource_builder
= IngressResourceV3Builder(
320 f
"{self.app.name}-ingress", annotations
323 if config
.ingress_whitelist_source_range
:
325 "nginx.ingress.kubernetes.io/whitelist-source-range"
326 ] = config
.ingress_whitelist_source_range
328 if config
.cluster_issuer
:
329 annotations
["cert-manager.io/cluster-issuer"] = config
.cluster_issuer
331 if parsed
.scheme
== "https":
332 ingress_resource_builder
.add_tls(
333 [parsed
.hostname
], config
.tls_secret_name
336 annotations
["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
338 ingress_resource_builder
.add_rule(parsed
.hostname
, self
.app
.name
, PORT
)
339 ingress_resource
= ingress_resource_builder
.build()
340 pod_spec_builder
.add_ingress_resource(ingress_resource
)
343 restart_policy
= PodRestartPolicy()
344 restart_policy
.add_secrets()
345 pod_spec_builder
.set_restart_policy(restart_policy
)
347 return pod_spec_builder
.build()
352 {"path": "/usr/lib/python3/dist-packages/osm_nbi"},
353 {"path": "/usr/lib/python3/dist-packages/osm_common"},
354 {"path": "/usr/lib/python3/dist-packages/osm_im"},
364 "module": "osm_nbi.nbi",
372 if __name__
== "__main__":