Adding ImagePullPolicy config option to OSM 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: Optional[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__(*args, oci_image="image")
123
124 self.kafka_client = KafkaClient(self, "kafka")
125 self.framework.observe(self.on["kafka"].relation_changed, self.configure_pod)
126 self.framework.observe(self.on["kafka"].relation_broken, self.configure_pod)
127
128 self.mongodb_client = MongoClient(self, "mongodb")
129 self.framework.observe(self.on["mongodb"].relation_changed, self.configure_pod)
130 self.framework.observe(self.on["mongodb"].relation_broken, self.configure_pod)
131
132 self.prometheus_client = PrometheusClient(self, "prometheus")
133 self.framework.observe(
134 self.on["prometheus"].relation_changed, self.configure_pod
135 )
136 self.framework.observe(
137 self.on["prometheus"].relation_broken, self.configure_pod
138 )
139
140 self.keystone_client = KeystoneClient(self, "keystone")
141 self.framework.observe(self.on["keystone"].relation_changed, self.configure_pod)
142 self.framework.observe(self.on["keystone"].relation_broken, self.configure_pod)
143
144 def _check_missing_dependencies(self, config: ConfigModel):
145 missing_relations = []
146
147 if self.kafka_client.is_missing_data_in_unit():
148 missing_relations.append("kafka")
149 if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit():
150 missing_relations.append("mongodb")
151 if self.prometheus_client.is_missing_data_in_app():
152 missing_relations.append("prometheus")
153 if config.keystone_enabled:
154 if self.keystone_client.is_missing_data_in_app():
155 missing_relations.append("keystone")
156
157 if missing_relations:
158 raise RelationsMissing(missing_relations)
159
160 def _build_cert_files(
161 self,
162 config: ConfigModel,
163 ):
164 cert_files_builder = FilesV3Builder()
165 for name, content in config.certificates_dict.items():
166 cert_files_builder.add_file(name, decode(content), mode=0o600)
167 return cert_files_builder.build()
168
169 def build_pod_spec(self, image_info):
170 # Validate config
171 config = ConfigModel(**dict(self.config))
172
173 if config.mongodb_uri and not self.mongodb_client.is_missing_data_in_unit():
174 raise Exception("Mongodb data cannot be provided via config and relation")
175
176 # Check relations
177 self._check_missing_dependencies(config)
178
179 # Create Builder for the PodSpec
180 pod_spec_builder = PodSpecV3Builder()
181
182 # Build Container
183 container_builder = ContainerV3Builder(
184 self.app.name, image_info, config.image_pull_policy
185 )
186 certs_files = self._build_cert_files(config)
187
188 if certs_files:
189 container_builder.add_volume_config("certs", "/certs", certs_files)
190
191 container_builder.add_port(name=self.app.name, port=PORT)
192 container_builder.add_envs(
193 {
194 # General configuration
195 "ALLOW_ANONYMOUS_LOGIN": "yes",
196 "OSMMON_OPENSTACK_DEFAULT_GRANULARITY": config.openstack_default_granularity,
197 "OSMMON_GLOBAL_REQUEST_TIMEOUT": config.global_request_timeout,
198 "OSMMON_GLOBAL_LOGLEVEL": config.log_level,
199 "OSMMON_COLLECTOR_INTERVAL": config.collector_interval,
200 "OSMMON_EVALUATOR_INTERVAL": config.evaluator_interval,
201 # Kafka configuration
202 "OSMMON_MESSAGE_DRIVER": "kafka",
203 "OSMMON_MESSAGE_HOST": self.kafka_client.host,
204 "OSMMON_MESSAGE_PORT": self.kafka_client.port,
205 # Database configuration
206 "OSMMON_DATABASE_DRIVER": "mongo",
207 "OSMMON_DATABASE_URI": config.mongodb_uri
208 or self.mongodb_client.connection_string,
209 "OSMMON_DATABASE_COMMONKEY": config.database_commonkey,
210 # Prometheus configuration
211 "OSMMON_PROMETHEUS_URL": f"http://{self.prometheus_client.hostname}:{self.prometheus_client.port}",
212 # VCA configuration
213 "OSMMON_VCA_HOST": config.vca_host,
214 "OSMMON_VCA_USER": config.vca_user,
215 "OSMMON_VCA_SECRET": config.vca_secret,
216 "OSMMON_VCA_CACERT": config.vca_cacert,
217 "OSMMON_GRAFANA_URL": config.grafana_url,
218 "OSMMON_GRAFANA_USER": config.grafana_user,
219 "OSMMON_GRAFANA_PASSWORD": config.grafana_password,
220 }
221 )
222 if config.keystone_enabled:
223 container_builder.add_envs(
224 {
225 "OSMMON_KEYSTONE_ENABLED": True,
226 "OSMMON_KEYSTONE_URL": self.keystone_client.host,
227 "OSMMON_KEYSTONE_DOMAIN_NAME": self.keystone_client.user_domain_name,
228 "OSMMON_KEYSTONE_PROJECT_DOMAIN_NAME": self.keystone_client.project_domain_name,
229 "OSMMON_KEYSTONE_SERVICE_USER": self.keystone_client.username,
230 "OSMMON_KEYSTONE_SERVICE_PASSWORD": self.keystone_client.password,
231 "OSMMON_KEYSTONE_SERVICE_PROJECT": self.keystone_client.service,
232 }
233 )
234 container = container_builder.build()
235
236 # Add container to pod spec
237 pod_spec_builder.add_container(container)
238
239 return pod_spec_builder.build()
240
241
242 if __name__ == "__main__":
243 main(MonCharm)