Adding VIM refresh period to RO charm config
[osm/devops.git] / installers / charm / ro / src / charm.py
1 #!/usr/bin/env python3
2 # Copyright 2021 Canonical Ltd.
3 #
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
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
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
14 # under the License.
15 #
16 # For those usages not covered by the Apache License, Version 2.0 please
17 # contact: legal@canonical.com
18 #
19 # To get in touch with the maintainers, please contact:
20 # osm-charmers@lists.launchpad.net
21 ##
22
23 # pylint: disable=E0213
24
25 import base64
26 import logging
27 from typing import Dict, NoReturn, Optional
28
29 from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
30 from ops.main import main
31 from opslib.osm.charm import CharmedOsmBase, RelationsMissing
32 from opslib.osm.interfaces.mongo import MongoClient
33 from opslib.osm.interfaces.mysql import MysqlClient
34 from opslib.osm.pod import (
35 ContainerV3Builder,
36 FilesV3Builder,
37 PodRestartPolicy,
38 PodSpecV3Builder,
39 )
40 from opslib.osm.validator import ModelValidator, validator
41
42 logger = logging.getLogger(__name__)
43
44 PORT = 9090
45
46
47 def _check_certificate_data(name: str, content: str):
48 if not name or not content:
49 raise ValueError("certificate name and content must be a non-empty string")
50
51
52 def _extract_certificates(certs_config: str):
53 certificates = {}
54 if certs_config:
55 cert_list = certs_config.split(",")
56 for cert in cert_list:
57 name, content = cert.split(":")
58 _check_certificate_data(name, content)
59 certificates[name] = content
60 return certificates
61
62
63 def decode(content: str):
64 return base64.b64decode(content.encode("utf-8")).decode("utf-8")
65
66
67 class ConfigModel(ModelValidator):
68 enable_ng_ro: bool
69 database_commonkey: str
70 mongodb_uri: Optional[str]
71 log_level: str
72 mysql_host: Optional[str]
73 mysql_port: Optional[int]
74 mysql_user: Optional[str]
75 mysql_password: Optional[str]
76 mysql_root_password: Optional[str]
77 vim_database: str
78 ro_database: str
79 openmano_tenant: str
80 certificates: Optional[str]
81 image_pull_policy: str
82 debug_mode: bool
83 security_context: bool
84 period_refresh_active: Optional[int]
85
86 @validator("log_level")
87 def validate_log_level(cls, v):
88 if v not in {"INFO", "DEBUG"}:
89 raise ValueError("value must be INFO or DEBUG")
90 return v
91
92 @validator("certificates")
93 def validate_certificates(cls, v):
94 # Raises an exception if it cannot extract the certificates
95 _extract_certificates(v)
96 return v
97
98 @validator("mongodb_uri")
99 def validate_mongodb_uri(cls, v):
100 if v and not v.startswith("mongodb://"):
101 raise ValueError("mongodb_uri is not properly formed")
102 return v
103
104 @validator("mysql_port")
105 def validate_mysql_port(cls, v):
106 if v and (v <= 0 or v >= 65535):
107 raise ValueError("Mysql port out of range")
108 return v
109
110 @validator("image_pull_policy")
111 def validate_image_pull_policy(cls, v):
112 values = {
113 "always": "Always",
114 "ifnotpresent": "IfNotPresent",
115 "never": "Never",
116 }
117 v = v.lower()
118 if v not in values.keys():
119 raise ValueError("value must be always, ifnotpresent or never")
120 return values[v]
121
122 @property
123 def certificates_dict(cls):
124 return _extract_certificates(cls.certificates) if cls.certificates else {}
125
126 @validator("period_refresh_active")
127 def validate_vim_refresh_period(cls, v):
128 if v and v < 60 and v != -1:
129 raise ValueError(
130 "Refresh Period is too tight, insert >= 60 seconds or disable using -1"
131 )
132 return v
133
134
135 class RoCharm(CharmedOsmBase):
136 """GrafanaCharm Charm."""
137
138 on = KafkaEvents()
139
140 def __init__(self, *args) -> NoReturn:
141 """Prometheus Charm constructor."""
142 super().__init__(
143 *args,
144 oci_image="image",
145 vscode_workspace=VSCODE_WORKSPACE,
146 )
147 if self.config.get("debug_mode"):
148 self.enable_debug_mode(
149 pubkey=self.config.get("debug_pubkey"),
150 hostpaths={
151 "osm_common": {
152 "hostpath": self.config.get("debug_common_local_path"),
153 "container-path": "/usr/lib/python3/dist-packages/osm_common",
154 },
155 **_get_ro_host_paths(self.config.get("debug_ro_local_path")),
156 },
157 )
158 self.kafka = KafkaRequires(self)
159 self.framework.observe(self.on.kafka_available, self.configure_pod)
160 self.framework.observe(self.on.kafka_broken, self.configure_pod)
161
162 self.mysql_client = MysqlClient(self, "mysql")
163 self.framework.observe(self.on["mysql"].relation_changed, self.configure_pod)
164 self.framework.observe(self.on["mysql"].relation_broken, self.configure_pod)
165
166 self.mongodb_client = MongoClient(self, "mongodb")
167 self.framework.observe(self.on["mongodb"].relation_changed, self.configure_pod)
168 self.framework.observe(self.on["mongodb"].relation_broken, self.configure_pod)
169
170 self.framework.observe(self.on["ro"].relation_joined, self._publish_ro_info)
171
172 def _publish_ro_info(self, event):
173 """Publishes RO information.
174
175 Args:
176 event (EventBase): RO relation event.
177 """
178 if self.unit.is_leader():
179 rel_data = {
180 "host": self.model.app.name,
181 "port": str(PORT),
182 }
183 for k, v in rel_data.items():
184 event.relation.data[self.app][k] = v
185
186 def _check_missing_dependencies(self, config: ConfigModel):
187 missing_relations = []
188
189 if config.enable_ng_ro:
190 if not self.kafka.host or not self.kafka.port:
191 missing_relations.append("kafka")
192 if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit():
193 missing_relations.append("mongodb")
194 else:
195 if not config.mysql_host and self.mysql_client.is_missing_data_in_unit():
196 missing_relations.append("mysql")
197 if missing_relations:
198 raise RelationsMissing(missing_relations)
199
200 def _validate_mysql_config(self, config: ConfigModel):
201 invalid_values = []
202 if not config.mysql_user:
203 invalid_values.append("Mysql user is empty")
204 if not config.mysql_password:
205 invalid_values.append("Mysql password is empty")
206 if not config.mysql_root_password:
207 invalid_values.append("Mysql root password empty")
208
209 if invalid_values:
210 raise ValueError("Invalid values: " + ", ".join(invalid_values))
211
212 def _build_cert_files(
213 self,
214 config: ConfigModel,
215 ):
216 cert_files_builder = FilesV3Builder()
217 for name, content in config.certificates_dict.items():
218 cert_files_builder.add_file(name, decode(content), mode=0o600)
219 return cert_files_builder.build()
220
221 def build_pod_spec(self, image_info):
222 # Validate config
223 config = ConfigModel(**dict(self.config))
224
225 if config.enable_ng_ro:
226 if config.mongodb_uri and not self.mongodb_client.is_missing_data_in_unit():
227 raise Exception(
228 "Mongodb data cannot be provided via config and relation"
229 )
230 else:
231 if config.mysql_host and not self.mysql_client.is_missing_data_in_unit():
232 raise Exception("Mysql data cannot be provided via config and relation")
233
234 if config.mysql_host:
235 self._validate_mysql_config(config)
236
237 # Check relations
238 self._check_missing_dependencies(config)
239
240 security_context_enabled = (
241 config.security_context if not config.debug_mode else False
242 )
243
244 # Create Builder for the PodSpec
245 pod_spec_builder = PodSpecV3Builder(
246 enable_security_context=security_context_enabled
247 )
248
249 # Build Container
250 container_builder = ContainerV3Builder(
251 self.app.name,
252 image_info,
253 config.image_pull_policy,
254 run_as_non_root=security_context_enabled,
255 )
256 certs_files = self._build_cert_files(config)
257
258 if certs_files:
259 container_builder.add_volume_config("certs", "/certs", certs_files)
260
261 container_builder.add_port(name=self.app.name, port=PORT)
262 container_builder.add_http_readiness_probe(
263 "/ro/" if config.enable_ng_ro else "/openmano/tenants",
264 PORT,
265 initial_delay_seconds=10,
266 period_seconds=10,
267 timeout_seconds=5,
268 failure_threshold=3,
269 )
270 container_builder.add_http_liveness_probe(
271 "/ro/" if config.enable_ng_ro else "/openmano/tenants",
272 PORT,
273 initial_delay_seconds=600,
274 period_seconds=10,
275 timeout_seconds=5,
276 failure_threshold=3,
277 )
278 container_builder.add_envs(
279 {
280 "OSMRO_LOG_LEVEL": config.log_level,
281 }
282 )
283 if config.period_refresh_active:
284 container_builder.add_envs(
285 {
286 "OSMRO_PERIOD_REFRESH_ACTIVE": config.period_refresh_active,
287 }
288 )
289 if config.enable_ng_ro:
290 # Add secrets to the pod
291 mongodb_secret_name = f"{self.app.name}-mongodb-secret"
292 pod_spec_builder.add_secret(
293 mongodb_secret_name,
294 {
295 "uri": config.mongodb_uri or self.mongodb_client.connection_string,
296 "commonkey": config.database_commonkey,
297 },
298 )
299 container_builder.add_envs(
300 {
301 "OSMRO_MESSAGE_DRIVER": "kafka",
302 "OSMRO_MESSAGE_HOST": self.kafka.host,
303 "OSMRO_MESSAGE_PORT": self.kafka.port,
304 # MongoDB configuration
305 "OSMRO_DATABASE_DRIVER": "mongo",
306 }
307 )
308 container_builder.add_secret_envs(
309 secret_name=mongodb_secret_name,
310 envs={
311 "OSMRO_DATABASE_URI": "uri",
312 "OSMRO_DATABASE_COMMONKEY": "commonkey",
313 },
314 )
315 restart_policy = PodRestartPolicy()
316 restart_policy.add_secrets(secret_names=(mongodb_secret_name,))
317 pod_spec_builder.set_restart_policy(restart_policy)
318
319 else:
320 container_builder.add_envs(
321 {
322 "RO_DB_HOST": config.mysql_host or self.mysql_client.host,
323 "RO_DB_OVIM_HOST": config.mysql_host or self.mysql_client.host,
324 "RO_DB_PORT": config.mysql_port or self.mysql_client.port,
325 "RO_DB_OVIM_PORT": config.mysql_port or self.mysql_client.port,
326 "RO_DB_USER": config.mysql_user or self.mysql_client.user,
327 "RO_DB_OVIM_USER": config.mysql_user or self.mysql_client.user,
328 "RO_DB_PASSWORD": config.mysql_password
329 or self.mysql_client.password,
330 "RO_DB_OVIM_PASSWORD": config.mysql_password
331 or self.mysql_client.password,
332 "RO_DB_ROOT_PASSWORD": config.mysql_root_password
333 or self.mysql_client.root_password,
334 "RO_DB_OVIM_ROOT_PASSWORD": config.mysql_root_password
335 or self.mysql_client.root_password,
336 "RO_DB_NAME": config.ro_database,
337 "RO_DB_OVIM_NAME": config.vim_database,
338 "OPENMANO_TENANT": config.openmano_tenant,
339 }
340 )
341 container = container_builder.build()
342
343 # Add container to pod spec
344 pod_spec_builder.add_container(container)
345
346 return pod_spec_builder.build()
347
348
349 VSCODE_WORKSPACE = {
350 "folders": [
351 {"path": "/usr/lib/python3/dist-packages/osm_ng_ro"},
352 {"path": "/usr/lib/python3/dist-packages/osm_common"},
353 {"path": "/usr/lib/python3/dist-packages/osm_ro_plugin"},
354 {"path": "/usr/lib/python3/dist-packages/osm_rosdn_arista_cloudvision"},
355 {"path": "/usr/lib/python3/dist-packages/osm_rosdn_dpb"},
356 {"path": "/usr/lib/python3/dist-packages/osm_rosdn_dynpac"},
357 {"path": "/usr/lib/python3/dist-packages/osm_rosdn_floodlightof"},
358 {"path": "/usr/lib/python3/dist-packages/osm_rosdn_ietfl2vpn"},
359 {"path": "/usr/lib/python3/dist-packages/osm_rosdn_juniper_contrail"},
360 {"path": "/usr/lib/python3/dist-packages/osm_rosdn_odlof"},
361 {"path": "/usr/lib/python3/dist-packages/osm_rosdn_onos_vpls"},
362 {"path": "/usr/lib/python3/dist-packages/osm_rosdn_onosof"},
363 {"path": "/usr/lib/python3/dist-packages/osm_rovim_aws"},
364 {"path": "/usr/lib/python3/dist-packages/osm_rovim_azure"},
365 {"path": "/usr/lib/python3/dist-packages/osm_rovim_gcp"},
366 {"path": "/usr/lib/python3/dist-packages/osm_rovim_fos"},
367 # {"path": "/usr/lib/python3/dist-packages/osm_rovim_opennebula"},
368 {"path": "/usr/lib/python3/dist-packages/osm_rovim_openstack"},
369 {"path": "/usr/lib/python3/dist-packages/osm_rovim_openvim"},
370 {"path": "/usr/lib/python3/dist-packages/osm_rovim_vmware"},
371 ],
372 "launch": {
373 "configurations": [
374 {
375 "module": "osm_ng_ro.ro_main",
376 "name": "NG RO",
377 "request": "launch",
378 "type": "python",
379 "justMyCode": False,
380 }
381 ],
382 "version": "0.2.0",
383 },
384 "settings": {},
385 }
386
387
388 def _get_ro_host_paths(ro_host_path: str) -> Dict:
389 """Get RO host paths"""
390 return (
391 {
392 "NG-RO": {
393 "hostpath": f"{ro_host_path}/NG-RO",
394 "container-path": "/usr/lib/python3/dist-packages/osm_ng_ro",
395 },
396 "RO-plugin": {
397 "hostpath": f"{ro_host_path}/RO-plugin",
398 "container-path": "/usr/lib/python3/dist-packages/osm_ro_plugin",
399 },
400 "RO-SDN-arista_cloudvision": {
401 "hostpath": f"{ro_host_path}/RO-SDN-arista_cloudvision",
402 "container-path": "/usr/lib/python3/dist-packages/osm_rosdn_arista_cloudvision",
403 },
404 "RO-SDN-dpb": {
405 "hostpath": f"{ro_host_path}/RO-SDN-dpb",
406 "container-path": "/usr/lib/python3/dist-packages/osm_rosdn_dpb",
407 },
408 "RO-SDN-dynpac": {
409 "hostpath": f"{ro_host_path}/RO-SDN-dynpac",
410 "container-path": "/usr/lib/python3/dist-packages/osm_rosdn_dynpac",
411 },
412 "RO-SDN-floodlight_openflow": {
413 "hostpath": f"{ro_host_path}/RO-SDN-floodlight_openflow",
414 "container-path": "/usr/lib/python3/dist-packages/osm_rosdn_floodlightof",
415 },
416 "RO-SDN-ietfl2vpn": {
417 "hostpath": f"{ro_host_path}/RO-SDN-ietfl2vpn",
418 "container-path": "/usr/lib/python3/dist-packages/osm_rosdn_ietfl2vpn",
419 },
420 "RO-SDN-juniper_contrail": {
421 "hostpath": f"{ro_host_path}/RO-SDN-juniper_contrail",
422 "container-path": "/usr/lib/python3/dist-packages/osm_rosdn_juniper_contrail",
423 },
424 "RO-SDN-odl_openflow": {
425 "hostpath": f"{ro_host_path}/RO-SDN-odl_openflow",
426 "container-path": "/usr/lib/python3/dist-packages/osm_rosdn_odlof",
427 },
428 "RO-SDN-onos_openflow": {
429 "hostpath": f"{ro_host_path}/RO-SDN-onos_openflow",
430 "container-path": "/usr/lib/python3/dist-packages/osm_rosdn_onosof",
431 },
432 "RO-SDN-onos_vpls": {
433 "hostpath": f"{ro_host_path}/RO-SDN-onos_vpls",
434 "container-path": "/usr/lib/python3/dist-packages/osm_rosdn_onos_vpls",
435 },
436 "RO-VIM-aws": {
437 "hostpath": f"{ro_host_path}/RO-VIM-aws",
438 "container-path": "/usr/lib/python3/dist-packages/osm_rovim_aws",
439 },
440 "RO-VIM-azure": {
441 "hostpath": f"{ro_host_path}/RO-VIM-azure",
442 "container-path": "/usr/lib/python3/dist-packages/osm_rovim_azure",
443 },
444 "RO-VIM-gcp": {
445 "hostpath": f"{ro_host_path}/RO-VIM-gcp",
446 "container-path": "/usr/lib/python3/dist-packages/osm_rovim_gcp",
447 },
448 "RO-VIM-fos": {
449 "hostpath": f"{ro_host_path}/RO-VIM-fos",
450 "container-path": "/usr/lib/python3/dist-packages/osm_rovim_fos",
451 },
452 "RO-VIM-opennebula": {
453 "hostpath": f"{ro_host_path}/RO-VIM-opennebula",
454 "container-path": "/usr/lib/python3/dist-packages/osm_rovim_opennebula",
455 },
456 "RO-VIM-openstack": {
457 "hostpath": f"{ro_host_path}/RO-VIM-openstack",
458 "container-path": "/usr/lib/python3/dist-packages/osm_rovim_openstack",
459 },
460 "RO-VIM-openvim": {
461 "hostpath": f"{ro_host_path}/RO-VIM-openvim",
462 "container-path": "/usr/lib/python3/dist-packages/osm_rovim_openvim",
463 },
464 "RO-VIM-vmware": {
465 "hostpath": f"{ro_host_path}/RO-VIM-vmware",
466 "container-path": "/usr/lib/python3/dist-packages/osm_rovim_vmware",
467 },
468 }
469 if ro_host_path
470 else {}
471 )
472
473
474 if __name__ == "__main__":
475 main(RoCharm)