Fix validation error for ImagePullPolicy in charms
[osm/devops.git] / installers / charm / mon / 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
26 import base64
27 import logging
28 from typing import NoReturn, Optional
29
30
31 from ops.main import main
32 from opslib.osm.charm import CharmedOsmBase, RelationsMissing
33 from opslib.osm.interfaces.kafka import KafkaClient
34 from opslib.osm.interfaces.keystone import KeystoneClient
35 from opslib.osm.interfaces.mongo import MongoClient
36 from opslib.osm.interfaces.prometheus import PrometheusClient
37 from opslib.osm.pod import ContainerV3Builder, FilesV3Builder, PodSpecV3Builder
38 from opslib.osm.validator import ModelValidator, validator
39
40
41 logger = logging.getLogger(__name__)
42
43 PORT = 8000
44
45
46 def _check_certificate_data(name: str, content: str):
47 if not name or not content:
48 raise ValueError("certificate name and content must be a non-empty string")
49
50
51 def _extract_certificates(certs_config: str):
52 certificates = {}
53 if certs_config:
54 cert_list = certs_config.split(",")
55 for cert in cert_list:
56 name, content = cert.split(":")
57 _check_certificate_data(name, content)
58 certificates[name] = content
59 return certificates
60
61
62 def decode(content: str):
63 return base64.b64decode(content.encode("utf-8")).decode("utf-8")
64
65
66 class ConfigModel(ModelValidator):
67 keystone_enabled: bool
68 vca_host: str
69 vca_user: str
70 vca_secret: str
71 vca_cacert: str
72 database_commonkey: str
73 mongodb_uri: Optional[str]
74 log_level: str
75 openstack_default_granularity: int
76 global_request_timeout: int
77 collector_interval: int
78 evaluator_interval: int
79 grafana_url: str
80 grafana_user: str
81 grafana_password: str
82 certificates: Optional[str]
83 image_pull_policy: str
84
85 @validator("log_level")
86 def validate_log_level(cls, v):
87 if v not in {"INFO", "DEBUG"}:
88 raise ValueError("value must be INFO or DEBUG")
89 return v
90
91 @validator("certificates")
92 def validate_certificates(cls, v):
93 # Raises an exception if it cannot extract the certificates
94 _extract_certificates(v)
95 return v
96
97 @validator("mongodb_uri")
98 def validate_mongodb_uri(cls, v):
99 if v and not v.startswith("mongodb://"):
100 raise ValueError("mongodb_uri is not properly formed")
101 return v
102
103 @validator("image_pull_policy")
104 def validate_image_pull_policy(cls, v):
105 values = {
106 "always": "Always",
107 "ifnotpresent": "IfNotPresent",
108 "never": "Never",
109 }
110 v = v.lower()
111 if v not in values.keys():
112 raise ValueError("value must be always, ifnotpresent or never")
113 return values[v]
114
115 @property
116 def certificates_dict(cls):
117 return _extract_certificates(cls.certificates) if cls.certificates else {}
118
119
120 class MonCharm(CharmedOsmBase):
121 def __init__(self, *args) -> NoReturn:
122 super().__init__(
123 *args,
124 oci_image="image",
125 debug_mode_config_key="debug_mode",
126 debug_pubkey_config_key="debug_pubkey",
127 vscode_workspace=VSCODE_WORKSPACE,
128 )
129
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)
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.prometheus_client = PrometheusClient(self, "prometheus")
139 self.framework.observe(
140 self.on["prometheus"].relation_changed, self.configure_pod
141 )
142 self.framework.observe(
143 self.on["prometheus"].relation_broken, self.configure_pod
144 )
145
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)
149
150 def _check_missing_dependencies(self, config: ConfigModel):
151 missing_relations = []
152
153 if self.kafka_client.is_missing_data_in_unit():
154 missing_relations.append("kafka")
155 if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit():
156 missing_relations.append("mongodb")
157 if self.prometheus_client.is_missing_data_in_app():
158 missing_relations.append("prometheus")
159 if config.keystone_enabled:
160 if self.keystone_client.is_missing_data_in_app():
161 missing_relations.append("keystone")
162
163 if missing_relations:
164 raise RelationsMissing(missing_relations)
165
166 def _build_cert_files(
167 self,
168 config: ConfigModel,
169 ):
170 cert_files_builder = FilesV3Builder()
171 for name, content in config.certificates_dict.items():
172 cert_files_builder.add_file(name, decode(content), mode=0o600)
173 return cert_files_builder.build()
174
175 def build_pod_spec(self, image_info):
176 # Validate config
177 config = ConfigModel(**dict(self.config))
178
179 if config.mongodb_uri and not self.mongodb_client.is_missing_data_in_unit():
180 raise Exception("Mongodb data cannot be provided via config and relation")
181
182 # Check relations
183 self._check_missing_dependencies(config)
184
185 # Create Builder for the PodSpec
186 pod_spec_builder = PodSpecV3Builder()
187
188 # Build Container
189 container_builder = ContainerV3Builder(
190 self.app.name, image_info, config.image_pull_policy
191 )
192 certs_files = self._build_cert_files(config)
193
194 if certs_files:
195 container_builder.add_volume_config("certs", "/certs", certs_files)
196
197 container_builder.add_port(name=self.app.name, port=PORT)
198 container_builder.add_envs(
199 {
200 # General configuration
201 "ALLOW_ANONYMOUS_LOGIN": "yes",
202 "OSMMON_OPENSTACK_DEFAULT_GRANULARITY": config.openstack_default_granularity,
203 "OSMMON_GLOBAL_REQUEST_TIMEOUT": config.global_request_timeout,
204 "OSMMON_GLOBAL_LOGLEVEL": config.log_level,
205 "OSMMON_COLLECTOR_INTERVAL": config.collector_interval,
206 "OSMMON_EVALUATOR_INTERVAL": config.evaluator_interval,
207 # Kafka configuration
208 "OSMMON_MESSAGE_DRIVER": "kafka",
209 "OSMMON_MESSAGE_HOST": self.kafka_client.host,
210 "OSMMON_MESSAGE_PORT": self.kafka_client.port,
211 # Database configuration
212 "OSMMON_DATABASE_DRIVER": "mongo",
213 "OSMMON_DATABASE_URI": config.mongodb_uri
214 or self.mongodb_client.connection_string,
215 "OSMMON_DATABASE_COMMONKEY": config.database_commonkey,
216 # Prometheus configuration
217 "OSMMON_PROMETHEUS_URL": f"http://{self.prometheus_client.hostname}:{self.prometheus_client.port}",
218 # VCA configuration
219 "OSMMON_VCA_HOST": config.vca_host,
220 "OSMMON_VCA_USER": config.vca_user,
221 "OSMMON_VCA_SECRET": config.vca_secret,
222 "OSMMON_VCA_CACERT": config.vca_cacert,
223 "OSMMON_GRAFANA_URL": config.grafana_url,
224 "OSMMON_GRAFANA_USER": config.grafana_user,
225 "OSMMON_GRAFANA_PASSWORD": config.grafana_password,
226 }
227 )
228 if config.keystone_enabled:
229 container_builder.add_envs(
230 {
231 "OSMMON_KEYSTONE_ENABLED": True,
232 "OSMMON_KEYSTONE_URL": self.keystone_client.host,
233 "OSMMON_KEYSTONE_DOMAIN_NAME": self.keystone_client.user_domain_name,
234 "OSMMON_KEYSTONE_PROJECT_DOMAIN_NAME": self.keystone_client.project_domain_name,
235 "OSMMON_KEYSTONE_SERVICE_USER": self.keystone_client.username,
236 "OSMMON_KEYSTONE_SERVICE_PASSWORD": self.keystone_client.password,
237 "OSMMON_KEYSTONE_SERVICE_PROJECT": self.keystone_client.service,
238 }
239 )
240 container = container_builder.build()
241
242 # Add container to pod spec
243 pod_spec_builder.add_container(container)
244
245 return pod_spec_builder.build()
246
247
248 VSCODE_WORKSPACE = {
249 "folders": [
250 {"path": "/usr/lib/python3/dist-packages/osm_mon"},
251 {"path": "/usr/lib/python3/dist-packages/osm_common"},
252 {"path": "/usr/lib/python3/dist-packages/n2vc"},
253 ],
254 "settings": {},
255 "launch": {
256 "version": "0.2.0",
257 "configurations": [
258 {
259 "name": "MON Server",
260 "type": "python",
261 "request": "launch",
262 "module": "osm_mon.cmd.mon_server",
263 "justMyCode": False,
264 },
265 {
266 "name": "MON evaluator",
267 "type": "python",
268 "request": "launch",
269 "module": "osm_mon.cmd.mon_evaluator",
270 "justMyCode": False,
271 },
272 {
273 "name": "MON collector",
274 "type": "python",
275 "request": "launch",
276 "module": "osm_mon.cmd.mon_collector",
277 "justMyCode": False,
278 },
279 {
280 "name": "MON dashboarder",
281 "type": "python",
282 "request": "launch",
283 "module": "osm_mon.cmd.mon_dashboarder",
284 "justMyCode": False,
285 },
286 ],
287 },
288 }
289 if __name__ == "__main__":
290 main(MonCharm)