3b6b7e2861bf182900b6f595150fe72bbfd86d46
[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 NoReturn, Optional
28
29 from ops.main import main
30 from opslib.osm.charm import CharmedOsmBase, RelationsMissing
31 from opslib.osm.interfaces.kafka import KafkaClient
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
83 @validator("log_level")
84 def validate_log_level(cls, v):
85 if v not in {"INFO", "DEBUG"}:
86 raise ValueError("value must be INFO or DEBUG")
87 return v
88
89 @validator("certificates")
90 def validate_certificates(cls, v):
91 # Raises an exception if it cannot extract the certificates
92 _extract_certificates(v)
93 return v
94
95 @validator("mongodb_uri")
96 def validate_mongodb_uri(cls, v):
97 if v and not v.startswith("mongodb://"):
98 raise ValueError("mongodb_uri is not properly formed")
99 return v
100
101 @validator("mysql_port")
102 def validate_mysql_port(cls, v):
103 if v and (v <= 0 or v >= 65535):
104 raise ValueError("Mysql port out of range")
105 return v
106
107 @validator("image_pull_policy")
108 def validate_image_pull_policy(cls, v):
109 values = {
110 "always": "Always",
111 "ifnotpresent": "IfNotPresent",
112 "never": "Never",
113 }
114 v = v.lower()
115 if v not in values.keys():
116 raise ValueError("value must be always, ifnotpresent or never")
117 return values[v]
118
119 @property
120 def certificates_dict(cls):
121 return _extract_certificates(cls.certificates) if cls.certificates else {}
122
123
124 class RoCharm(CharmedOsmBase):
125 """GrafanaCharm Charm."""
126
127 def __init__(self, *args) -> NoReturn:
128 """Prometheus Charm constructor."""
129 super().__init__(
130 *args,
131 oci_image="image",
132 debug_mode_config_key="debug_mode",
133 debug_pubkey_config_key="debug_pubkey",
134 vscode_workspace=VSCODE_WORKSPACE,
135 )
136
137 self.kafka_client = KafkaClient(self, "kafka")
138 self.framework.observe(self.on["kafka"].relation_changed, self.configure_pod)
139 self.framework.observe(self.on["kafka"].relation_broken, self.configure_pod)
140
141 self.mysql_client = MysqlClient(self, "mysql")
142 self.framework.observe(self.on["mysql"].relation_changed, self.configure_pod)
143 self.framework.observe(self.on["mysql"].relation_broken, self.configure_pod)
144
145 self.mongodb_client = MongoClient(self, "mongodb")
146 self.framework.observe(self.on["mongodb"].relation_changed, self.configure_pod)
147 self.framework.observe(self.on["mongodb"].relation_broken, self.configure_pod)
148
149 self.framework.observe(self.on["ro"].relation_joined, self._publish_ro_info)
150
151 def _publish_ro_info(self, event):
152 """Publishes RO information.
153
154 Args:
155 event (EventBase): RO relation event.
156 """
157 if self.unit.is_leader():
158 rel_data = {
159 "host": self.model.app.name,
160 "port": str(PORT),
161 }
162 for k, v in rel_data.items():
163 event.relation.data[self.app][k] = v
164
165 def _check_missing_dependencies(self, config: ConfigModel):
166 missing_relations = []
167
168 if config.enable_ng_ro:
169 if self.kafka_client.is_missing_data_in_unit():
170 missing_relations.append("kafka")
171 if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit():
172 missing_relations.append("mongodb")
173 else:
174 if not config.mysql_host and self.mysql_client.is_missing_data_in_unit():
175 missing_relations.append("mysql")
176 if missing_relations:
177 raise RelationsMissing(missing_relations)
178
179 def _validate_mysql_config(self, config: ConfigModel):
180 invalid_values = []
181 if not config.mysql_user:
182 invalid_values.append("Mysql user is empty")
183 if not config.mysql_password:
184 invalid_values.append("Mysql password is empty")
185 if not config.mysql_root_password:
186 invalid_values.append("Mysql root password empty")
187
188 if invalid_values:
189 raise ValueError("Invalid values: " + ", ".join(invalid_values))
190
191 def _build_cert_files(
192 self,
193 config: ConfigModel,
194 ):
195 cert_files_builder = FilesV3Builder()
196 for name, content in config.certificates_dict.items():
197 cert_files_builder.add_file(name, decode(content), mode=0o600)
198 return cert_files_builder.build()
199
200 def build_pod_spec(self, image_info):
201 # Validate config
202 config = ConfigModel(**dict(self.config))
203
204 if config.enable_ng_ro:
205 if config.mongodb_uri and not self.mongodb_client.is_missing_data_in_unit():
206 raise Exception(
207 "Mongodb data cannot be provided via config and relation"
208 )
209 else:
210 if config.mysql_host and not self.mysql_client.is_missing_data_in_unit():
211 raise Exception("Mysql data cannot be provided via config and relation")
212
213 if config.mysql_host:
214 self._validate_mysql_config(config)
215
216 # Check relations
217 self._check_missing_dependencies(config)
218
219 # Create Builder for the PodSpec
220 pod_spec_builder = PodSpecV3Builder()
221
222 # Build Container
223 container_builder = ContainerV3Builder(
224 self.app.name, image_info, config.image_pull_policy
225 )
226 certs_files = self._build_cert_files(config)
227
228 if certs_files:
229 container_builder.add_volume_config("certs", "/certs", certs_files)
230
231 container_builder.add_port(name=self.app.name, port=PORT)
232 container_builder.add_http_readiness_probe(
233 "/ro/" if config.enable_ng_ro else "/openmano/tenants",
234 PORT,
235 initial_delay_seconds=10,
236 period_seconds=10,
237 timeout_seconds=5,
238 failure_threshold=3,
239 )
240 container_builder.add_http_liveness_probe(
241 "/ro/" if config.enable_ng_ro else "/openmano/tenants",
242 PORT,
243 initial_delay_seconds=600,
244 period_seconds=10,
245 timeout_seconds=5,
246 failure_threshold=3,
247 )
248 container_builder.add_envs(
249 {
250 "OSMRO_LOG_LEVEL": config.log_level,
251 }
252 )
253
254 if config.enable_ng_ro:
255 # Add secrets to the pod
256 mongodb_secret_name = f"{self.app.name}-mongodb-secret"
257 pod_spec_builder.add_secret(
258 mongodb_secret_name,
259 {
260 "uri": config.mongodb_uri or self.mongodb_client.connection_string,
261 "commonkey": config.database_commonkey,
262 },
263 )
264 container_builder.add_envs(
265 {
266 "OSMRO_MESSAGE_DRIVER": "kafka",
267 "OSMRO_MESSAGE_HOST": self.kafka_client.host,
268 "OSMRO_MESSAGE_PORT": self.kafka_client.port,
269 # MongoDB configuration
270 "OSMRO_DATABASE_DRIVER": "mongo",
271 }
272 )
273 container_builder.add_secret_envs(
274 secret_name=mongodb_secret_name,
275 envs={
276 "OSMRO_DATABASE_URI": "uri",
277 "OSMRO_DATABASE_COMMONKEY": "commonkey",
278 },
279 )
280 restart_policy = PodRestartPolicy()
281 restart_policy.add_secrets(secret_names=(mongodb_secret_name,))
282 pod_spec_builder.set_restart_policy(restart_policy)
283
284 else:
285 container_builder.add_envs(
286 {
287 "RO_DB_HOST": config.mysql_host or self.mysql_client.host,
288 "RO_DB_OVIM_HOST": config.mysql_host or self.mysql_client.host,
289 "RO_DB_PORT": config.mysql_port or self.mysql_client.port,
290 "RO_DB_OVIM_PORT": config.mysql_port or self.mysql_client.port,
291 "RO_DB_USER": config.mysql_user or self.mysql_client.user,
292 "RO_DB_OVIM_USER": config.mysql_user or self.mysql_client.user,
293 "RO_DB_PASSWORD": config.mysql_password
294 or self.mysql_client.password,
295 "RO_DB_OVIM_PASSWORD": config.mysql_password
296 or self.mysql_client.password,
297 "RO_DB_ROOT_PASSWORD": config.mysql_root_password
298 or self.mysql_client.root_password,
299 "RO_DB_OVIM_ROOT_PASSWORD": config.mysql_root_password
300 or self.mysql_client.root_password,
301 "RO_DB_NAME": config.ro_database,
302 "RO_DB_OVIM_NAME": config.vim_database,
303 "OPENMANO_TENANT": config.openmano_tenant,
304 }
305 )
306 container = container_builder.build()
307
308 # Add container to pod spec
309 pod_spec_builder.add_container(container)
310
311 return pod_spec_builder.build()
312
313
314 VSCODE_WORKSPACE = {
315 "folders": [
316 {"path": "/usr/lib/python3/dist-packages/osm_ng_ro"},
317 {"path": "/usr/lib/python3/dist-packages/osm_common"},
318 {"path": "/usr/lib/python3/dist-packages/osm_ro_plugin"},
319 {"path": "/usr/lib/python3/dist-packages/osm_rosdn_arista_cloudvision"},
320 {"path": "/usr/lib/python3/dist-packages/osm_rosdn_dpb"},
321 {"path": "/usr/lib/python3/dist-packages/osm_rosdn_dynpac"},
322 {"path": "/usr/lib/python3/dist-packages/osm_rosdn_floodlightof"},
323 {"path": "/usr/lib/python3/dist-packages/osm_rosdn_ietfl2vpn"},
324 {"path": "/usr/lib/python3/dist-packages/osm_rosdn_juniper_contrail"},
325 {"path": "/usr/lib/python3/dist-packages/osm_rosdn_odlof"},
326 {"path": "/usr/lib/python3/dist-packages/osm_rosdn_onos_vpls"},
327 {"path": "/usr/lib/python3/dist-packages/osm_rosdn_onosof"},
328 {"path": "/usr/lib/python3/dist-packages/osm_rovim_aws"},
329 {"path": "/usr/lib/python3/dist-packages/osm_rovim_azure"},
330 {"path": "/usr/lib/python3/dist-packages/osm_rovim_fos"},
331 {"path": "/usr/lib/python3/dist-packages/osm_rovim_opennebula"},
332 {"path": "/usr/lib/python3/dist-packages/osm_rovim_openstack"},
333 {"path": "/usr/lib/python3/dist-packages/osm_rovim_openvim"},
334 {"path": "/usr/lib/python3/dist-packages/osm_rovim_vmware"},
335 ],
336 "launch": {
337 "configurations": [
338 {
339 "module": "osm_ng_ro.ro_main",
340 "name": "NG RO",
341 "request": "launch",
342 "type": "python",
343 "justMyCode": False,
344 }
345 ],
346 "version": "0.2.0",
347 },
348 "settings": {},
349 }
350
351 if __name__ == "__main__":
352 main(RoCharm)