Adding ImagePullPolicy config option to OSM Charms
[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 ContainerV3Builder, FilesV3Builder, PodSpecV3Builder
35 from opslib.osm.validator import ModelValidator, validator
36
37 logger = logging.getLogger(__name__)
38
39 PORT = 9090
40
41
42 def _check_certificate_data(name: str, content: str):
43 if not name or not content:
44 raise ValueError("certificate name and content must be a non-empty string")
45
46
47 def _extract_certificates(certs_config: str):
48 certificates = {}
49 if certs_config:
50 cert_list = certs_config.split(",")
51 for cert in cert_list:
52 name, content = cert.split(":")
53 _check_certificate_data(name, content)
54 certificates[name] = content
55 return certificates
56
57
58 def decode(content: str):
59 return base64.b64decode(content.encode("utf-8")).decode("utf-8")
60
61
62 class ConfigModel(ModelValidator):
63 enable_ng_ro: bool
64 database_commonkey: str
65 mongodb_uri: Optional[str]
66 log_level: str
67 mysql_host: Optional[str]
68 mysql_port: Optional[int]
69 mysql_user: Optional[str]
70 mysql_password: Optional[str]
71 mysql_root_password: Optional[str]
72 vim_database: str
73 ro_database: str
74 openmano_tenant: str
75 certificates: Optional[str]
76 image_pull_policy: Optional[str]
77
78 @validator("log_level")
79 def validate_log_level(cls, v):
80 if v not in {"INFO", "DEBUG"}:
81 raise ValueError("value must be INFO or DEBUG")
82 return v
83
84 @validator("certificates")
85 def validate_certificates(cls, v):
86 # Raises an exception if it cannot extract the certificates
87 _extract_certificates(v)
88 return v
89
90 @validator("mongodb_uri")
91 def validate_mongodb_uri(cls, v):
92 if v and not v.startswith("mongodb://"):
93 raise ValueError("mongodb_uri is not properly formed")
94 return v
95
96 @validator("mysql_port")
97 def validate_mysql_port(cls, v):
98 if v and (v <= 0 or v >= 65535):
99 raise ValueError("Mysql port out of range")
100 return v
101
102 @validator("image_pull_policy")
103 def validate_image_pull_policy(cls, v):
104 values = {
105 "always": "Always",
106 "ifnotpresent": "IfNotPresent",
107 "never": "Never",
108 }
109 v = v.lower()
110 if v not in values.keys():
111 raise ValueError("value must be always, ifnotpresent or never")
112 return values[v]
113
114 @property
115 def certificates_dict(cls):
116 return _extract_certificates(cls.certificates) if cls.certificates else {}
117
118
119 class RoCharm(CharmedOsmBase):
120 """GrafanaCharm Charm."""
121
122 def __init__(self, *args) -> NoReturn:
123 """Prometheus Charm constructor."""
124 super().__init__(*args, oci_image="image")
125
126 self.kafka_client = KafkaClient(self, "kafka")
127 self.framework.observe(self.on["kafka"].relation_changed, self.configure_pod)
128 self.framework.observe(self.on["kafka"].relation_broken, self.configure_pod)
129
130 self.mysql_client = MysqlClient(self, "mysql")
131 self.framework.observe(self.on["mysql"].relation_changed, self.configure_pod)
132 self.framework.observe(self.on["mysql"].relation_broken, self.configure_pod)
133
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)
137
138 self.framework.observe(self.on["ro"].relation_joined, self._publish_ro_info)
139
140 def _publish_ro_info(self, event):
141 """Publishes RO information.
142
143 Args:
144 event (EventBase): RO relation event.
145 """
146 if self.unit.is_leader():
147 rel_data = {
148 "host": self.model.app.name,
149 "port": str(PORT),
150 }
151 for k, v in rel_data.items():
152 event.relation.data[self.app][k] = v
153
154 def _check_missing_dependencies(self, config: ConfigModel):
155 missing_relations = []
156
157 if config.enable_ng_ro:
158 if self.kafka_client.is_missing_data_in_unit():
159 missing_relations.append("kafka")
160 if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit():
161 missing_relations.append("mongodb")
162 else:
163 if not config.mysql_host and self.mysql_client.is_missing_data_in_unit():
164 missing_relations.append("mysql")
165 if missing_relations:
166 raise RelationsMissing(missing_relations)
167
168 def _validate_mysql_config(self, config: ConfigModel):
169 invalid_values = []
170 if not config.mysql_user:
171 invalid_values.append("Mysql user is empty")
172 if not config.mysql_password:
173 invalid_values.append("Mysql password is empty")
174 if not config.mysql_root_password:
175 invalid_values.append("Mysql root password empty")
176
177 if invalid_values:
178 raise ValueError("Invalid values: " + ", ".join(invalid_values))
179
180 def _build_cert_files(
181 self,
182 config: ConfigModel,
183 ):
184 cert_files_builder = FilesV3Builder()
185 for name, content in config.certificates_dict.items():
186 cert_files_builder.add_file(name, decode(content), mode=0o600)
187 return cert_files_builder.build()
188
189 def build_pod_spec(self, image_info):
190 # Validate config
191 config = ConfigModel(**dict(self.config))
192
193 if config.enable_ng_ro:
194 if config.mongodb_uri and not self.mongodb_client.is_missing_data_in_unit():
195 raise Exception(
196 "Mongodb data cannot be provided via config and relation"
197 )
198 else:
199 if config.mysql_host and not self.mysql_client.is_missing_data_in_unit():
200 raise Exception("Mysql data cannot be provided via config and relation")
201
202 if config.mysql_host:
203 self._validate_mysql_config(config)
204
205 # Check relations
206 self._check_missing_dependencies(config)
207
208 # Create Builder for the PodSpec
209 pod_spec_builder = PodSpecV3Builder()
210
211 # Build Container
212 container_builder = ContainerV3Builder(
213 self.app.name, image_info, config.image_pull_policy
214 )
215 certs_files = self._build_cert_files(config)
216
217 if certs_files:
218 container_builder.add_volume_config("certs", "/certs", certs_files)
219
220 container_builder.add_port(name=self.app.name, port=PORT)
221 container_builder.add_http_readiness_probe(
222 "/ro/" if config.enable_ng_ro else "/openmano/tenants",
223 PORT,
224 initial_delay_seconds=10,
225 period_seconds=10,
226 timeout_seconds=5,
227 failure_threshold=3,
228 )
229 container_builder.add_http_liveness_probe(
230 "/ro/" if config.enable_ng_ro else "/openmano/tenants",
231 PORT,
232 initial_delay_seconds=600,
233 period_seconds=10,
234 timeout_seconds=5,
235 failure_threshold=3,
236 )
237 container_builder.add_envs(
238 {
239 "OSMRO_LOG_LEVEL": config.log_level,
240 }
241 )
242
243 if config.enable_ng_ro:
244 container_builder.add_envs(
245 {
246 "OSMRO_MESSAGE_DRIVER": "kafka",
247 "OSMRO_MESSAGE_HOST": self.kafka_client.host,
248 "OSMRO_MESSAGE_PORT": self.kafka_client.port,
249 # MongoDB configuration
250 "OSMRO_DATABASE_DRIVER": "mongo",
251 "OSMRO_DATABASE_URI": config.mongodb_uri
252 or self.mongodb_client.connection_string,
253 "OSMRO_DATABASE_COMMONKEY": config.database_commonkey,
254 }
255 )
256
257 else:
258 container_builder.add_envs(
259 {
260 "RO_DB_HOST": config.mysql_host or self.mysql_client.host,
261 "RO_DB_OVIM_HOST": config.mysql_host or self.mysql_client.host,
262 "RO_DB_PORT": config.mysql_port or self.mysql_client.port,
263 "RO_DB_OVIM_PORT": config.mysql_port or self.mysql_client.port,
264 "RO_DB_USER": config.mysql_user or self.mysql_client.user,
265 "RO_DB_OVIM_USER": config.mysql_user or self.mysql_client.user,
266 "RO_DB_PASSWORD": config.mysql_password
267 or self.mysql_client.password,
268 "RO_DB_OVIM_PASSWORD": config.mysql_password
269 or self.mysql_client.password,
270 "RO_DB_ROOT_PASSWORD": config.mysql_root_password
271 or self.mysql_client.root_password,
272 "RO_DB_OVIM_ROOT_PASSWORD": config.mysql_root_password
273 or self.mysql_client.root_password,
274 "RO_DB_NAME": config.ro_database,
275 "RO_DB_OVIM_NAME": config.vim_database,
276 "OPENMANO_TENANT": config.openmano_tenant,
277 }
278 )
279 container = container_builder.build()
280
281 # Add container to pod spec
282 pod_spec_builder.add_container(container)
283
284 return pod_spec_builder.build()
285
286
287 if __name__ == "__main__":
288 main(RoCharm)