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
= []
165 if self
.kafka_client
.is_missing_data_in_unit():
166 missing_relations
.append("kafka")
167 if not config
.mongodb_uri
and self
.mongodb_client
.is_missing_data_in_unit():
168 missing_relations
.append("mongodb")
169 if self
.prometheus_client
.is_missing_data_in_app():
170 missing_relations
.append("prometheus")
171 if config
.auth_backend
== "keystone":
172 if self
.keystone_client
.is_missing_data_in_app():
173 missing_relations
.append("keystone")
175 if missing_relations
:
176 raise RelationsMissing(missing_relations
)
178 def build_pod_spec(self
, image_info
):
180 config
= ConfigModel(**dict(self
.config
))
182 if config
.mongodb_uri
and not self
.mongodb_client
.is_missing_data_in_unit():
183 raise Exception("Mongodb data cannot be provided via config and relation")
186 self
._check
_missing
_dependencies
(config
)
188 security_context_enabled
= (
189 config
.security_context
if not config
.debug_mode
else False
192 # Create Builder for the PodSpec
193 pod_spec_builder
= PodSpecV3Builder(
194 enable_security_context
=security_context_enabled
197 # Add secrets to the pod
198 mongodb_secret_name
= f
"{self.app.name}-mongodb-secret"
199 pod_spec_builder
.add_secret(
202 "uri": config
.mongodb_uri
or self
.mongodb_client
.connection_string
,
203 "commonkey": config
.database_commonkey
,
207 # Build Init Container
208 pod_spec_builder
.add_init_container(
210 "name": "init-check",
211 "image": "alpine:latest",
215 f
"until (nc -zvw1 {self.kafka_client.host} {self.kafka_client.port} ); do sleep 3; done; exit 0",
221 container_builder
= ContainerV3Builder(
224 config
.image_pull_policy
,
225 run_as_non_root
=security_context_enabled
,
227 container_builder
.add_port(name
=self
.app
.name
, port
=PORT
)
228 container_builder
.add_tcpsocket_readiness_probe(
230 initial_delay_seconds
=5,
233 container_builder
.add_tcpsocket_liveness_probe(
235 initial_delay_seconds
=45,
238 container_builder
.add_envs(
240 # General configuration
241 "ALLOW_ANONYMOUS_LOGIN": "yes",
242 "OSMNBI_SERVER_ENABLE_TEST": config
.enable_test
,
243 "OSMNBI_STATIC_DIR": "/app/osm_nbi/html_public",
244 # Kafka configuration
245 "OSMNBI_MESSAGE_HOST": self
.kafka_client
.host
,
246 "OSMNBI_MESSAGE_DRIVER": "kafka",
247 "OSMNBI_MESSAGE_PORT": self
.kafka_client
.port
,
248 # Database configuration
249 "OSMNBI_DATABASE_DRIVER": "mongo",
250 # Storage configuration
251 "OSMNBI_STORAGE_DRIVER": "mongo",
252 "OSMNBI_STORAGE_PATH": "/app/storage",
253 "OSMNBI_STORAGE_COLLECTION": "files",
254 # Prometheus configuration
255 "OSMNBI_PROMETHEUS_HOST": self
.prometheus_client
.hostname
,
256 "OSMNBI_PROMETHEUS_PORT": self
.prometheus_client
.port
,
258 "OSMNBI_LOG_LEVEL": config
.log_level
,
261 container_builder
.add_secret_envs(
262 secret_name
=mongodb_secret_name
,
264 "OSMNBI_DATABASE_URI": "uri",
265 "OSMNBI_DATABASE_COMMONKEY": "commonkey",
266 "OSMNBI_STORAGE_URI": "uri",
269 if config
.auth_backend
== "internal":
270 container_builder
.add_env("OSMNBI_AUTHENTICATION_BACKEND", "internal")
271 elif config
.auth_backend
== "keystone":
272 keystone_secret_name
= f
"{self.app.name}-keystone-secret"
273 pod_spec_builder
.add_secret(
274 keystone_secret_name
,
276 "url": self
.keystone_client
.host
,
277 "port": self
.keystone_client
.port
,
278 "user_domain": self
.keystone_client
.user_domain_name
,
279 "project_domain": self
.keystone_client
.project_domain_name
,
280 "service_username": self
.keystone_client
.username
,
281 "service_password": self
.keystone_client
.password
,
282 "service_project": self
.keystone_client
.service
,
285 container_builder
.add_env("OSMNBI_AUTHENTICATION_BACKEND", "keystone")
286 container_builder
.add_secret_envs(
287 secret_name
=keystone_secret_name
,
289 "OSMNBI_AUTHENTICATION_AUTH_URL": "url",
290 "OSMNBI_AUTHENTICATION_AUTH_PORT": "port",
291 "OSMNBI_AUTHENTICATION_USER_DOMAIN_NAME": "user_domain",
292 "OSMNBI_AUTHENTICATION_PROJECT_DOMAIN_NAME": "project_domain",
293 "OSMNBI_AUTHENTICATION_SERVICE_USERNAME": "service_username",
294 "OSMNBI_AUTHENTICATION_SERVICE_PASSWORD": "service_password",
295 "OSMNBI_AUTHENTICATION_SERVICE_PROJECT": "service_project",
298 container
= container_builder
.build()
300 # Add container to pod spec
301 pod_spec_builder
.add_container(container
)
303 # Add ingress resources to pod spec if site url exists
305 parsed
= urlparse(config
.site_url
)
307 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
308 str(config
.max_file_size
) + "m"
309 if config
.max_file_size
> 0
310 else config
.max_file_size
312 "nginx.ingress.kubernetes.io/backend-protocol": "HTTPS",
314 if config
.ingress_class
:
315 annotations
["kubernetes.io/ingress.class"] = config
.ingress_class
316 ingress_resource_builder
= IngressResourceV3Builder(
317 f
"{self.app.name}-ingress", annotations
320 if config
.ingress_whitelist_source_range
:
322 "nginx.ingress.kubernetes.io/whitelist-source-range"
323 ] = config
.ingress_whitelist_source_range
325 if config
.cluster_issuer
:
326 annotations
["cert-manager.io/cluster-issuer"] = config
.cluster_issuer
328 if parsed
.scheme
== "https":
329 ingress_resource_builder
.add_tls(
330 [parsed
.hostname
], config
.tls_secret_name
333 annotations
["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
335 ingress_resource_builder
.add_rule(parsed
.hostname
, self
.app
.name
, PORT
)
336 ingress_resource
= ingress_resource_builder
.build()
337 pod_spec_builder
.add_ingress_resource(ingress_resource
)
340 restart_policy
= PodRestartPolicy()
341 restart_policy
.add_secrets()
342 pod_spec_builder
.set_restart_policy(restart_policy
)
344 return pod_spec_builder
.build()
349 {"path": "/usr/lib/python3/dist-packages/osm_nbi"},
350 {"path": "/usr/lib/python3/dist-packages/osm_common"},
351 {"path": "/usr/lib/python3/dist-packages/osm_im"},
361 "module": "osm_nbi.nbi",
369 if __name__
== "__main__":