Add keystone charm and interface
[osm/devops.git] / installers / charm / keystone / src / charm.py
1 #!/usr/bin/env python3
2 # Copyright 2020 Canonical Ltd.
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain 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,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15
16 import logging
17
18 from urllib.parse import urlparse
19
20 from ops.charm import CharmBase
21
22 # from ops.framework import StoredState
23 from ops.main import main
24 from ops.model import (
25 ActiveStatus,
26 BlockedStatus,
27 # MaintenanceStatus,
28 WaitingStatus,
29 # ModelError,
30 )
31 from ops.framework import StoredState
32
33 logger = logging.getLogger(__name__)
34
35 REQUIRED_SETTINGS = []
36
37 DATABASE_NAME = "keystone" # This is hardcoded in the keystone container script
38 # We expect the keystone container to use the default port
39 KEYSTONE_PORT = 5000
40
41
42 class KeystoneCharm(CharmBase):
43
44 state = StoredState()
45
46 def __init__(self, *args):
47 super().__init__(*args)
48
49 # Register all of the events we want to observe
50 self.framework.observe(self.on.config_changed, self.configure_pod)
51 self.framework.observe(self.on.start, self.configure_pod)
52 self.framework.observe(self.on.upgrade_charm, self.configure_pod)
53
54 # Register relation events
55 self.state.set_default(
56 db_host=None, db_port=None, db_user=None, db_password=None
57 )
58 self.framework.observe(
59 self.on.db_relation_changed, self._on_db_relation_changed
60 )
61 self.framework.observe(
62 self.on.keystone_relation_joined, self._publish_keystone_info
63 )
64
65 def _publish_keystone_info(self, event):
66 config = self.model.config
67 if self.unit.is_leader():
68 rel_data = {
69 "host": f"http://{self.app.name}:{KEYSTONE_PORT}/v3",
70 "port": str(KEYSTONE_PORT),
71 "keystone_db_password": config["keystone_db_password"],
72 "region_id": config["region_id"],
73 "user_domain_name": config["user_domain_name"],
74 "project_domain_name": config["project_domain_name"],
75 "admin_username": config["admin_username"],
76 "admin_password": config["admin_password"],
77 "admin_project_name": config["admin_project"],
78 "username": config["service_username"],
79 "password": config["service_password"],
80 "service": config["service_project"],
81 }
82 for k, v in rel_data.items():
83 event.relation.data[self.model.unit][k] = v
84
85 def _on_db_relation_changed(self, event):
86 self.state.db_host = event.relation.data[event.unit].get("host")
87 self.state.db_port = event.relation.data[event.unit].get("port", 3306)
88 self.state.db_user = "root" # event.relation.data[event.unit].get("user")
89 self.state.db_password = event.relation.data[event.unit].get("root_password")
90 if self.state.db_host:
91 self.configure_pod(event)
92
93 def _check_settings(self):
94 problems = []
95 config = self.model.config
96
97 for setting in REQUIRED_SETTINGS:
98 if not config.get(setting):
99 problem = f"missing config {setting}"
100 problems.append(problem)
101
102 return ";".join(problems)
103
104 def _make_pod_image_details(self):
105 config = self.model.config
106 image_details = {
107 "imagePath": config["image"],
108 }
109 if config["image_username"]:
110 image_details.update(
111 {
112 "username": config["image_username"],
113 "password": config["image_password"],
114 }
115 )
116 return image_details
117
118 def _make_pod_ports(self):
119 return [
120 {"name": "keystone", "containerPort": KEYSTONE_PORT, "protocol": "TCP"},
121 ]
122
123 def _make_pod_envconfig(self):
124 config = self.model.config
125
126 return {
127 "DB_HOST": self.state.db_host,
128 "DB_PORT": self.state.db_port,
129 "ROOT_DB_USER": self.state.db_user,
130 "ROOT_DB_PASSWORD": self.state.db_password,
131 "KEYSTONE_DB_PASSWORD": config["keystone_db_password"],
132 "REGION_ID": config["region_id"],
133 "KEYSTONE_HOST": self.app.name,
134 "ADMIN_USERNAME": config["admin_username"],
135 "ADMIN_PASSWORD": config["admin_password"],
136 "ADMIN_PROJECT": config["admin_project"],
137 "SERVICE_USERNAME": config["service_username"],
138 "SERVICE_PASSWORD": config["service_password"],
139 "SERVICE_PROJECT": config["service_project"],
140 }
141
142 def _make_pod_ingress_resources(self):
143 site_url = self.model.config["site_url"]
144
145 if not site_url:
146 return
147
148 parsed = urlparse(site_url)
149
150 if not parsed.scheme.startswith("http"):
151 return
152
153 max_file_size = self.model.config["max_file_size"]
154 ingress_whitelist_source_range = self.model.config[
155 "ingress_whitelist_source_range"
156 ]
157
158 annotations = {
159 "nginx.ingress.kubernetes.io/proxy-body-size": "{}m".format(max_file_size)
160 }
161
162 if ingress_whitelist_source_range:
163 annotations[
164 "nginx.ingress.kubernetes.io/whitelist-source-range"
165 ] = ingress_whitelist_source_range
166
167 ingress_spec_tls = None
168
169 if parsed.scheme == "https":
170 ingress_spec_tls = [{"hosts": [parsed.hostname]}]
171 tls_secret_name = self.model.config["tls_secret_name"]
172 if tls_secret_name:
173 ingress_spec_tls[0]["secretName"] = tls_secret_name
174 else:
175 annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
176
177 ingress = {
178 "name": "{}-ingress".format(self.app.name),
179 "annotations": annotations,
180 "spec": {
181 "rules": [
182 {
183 "host": parsed.hostname,
184 "http": {
185 "paths": [
186 {
187 "path": "/",
188 "backend": {
189 "serviceName": self.app.name,
190 "servicePort": KEYSTONE_PORT,
191 },
192 }
193 ]
194 },
195 }
196 ],
197 },
198 }
199 if ingress_spec_tls:
200 ingress["spec"]["tls"] = ingress_spec_tls
201
202 return [ingress]
203
204 def configure_pod(self, event):
205 """Assemble the pod spec and apply it, if possible."""
206
207 if not self.state.db_host:
208 self.unit.status = WaitingStatus("Waiting for database relation")
209 event.defer()
210 return
211
212 if not self.unit.is_leader():
213 self.unit.status = ActiveStatus()
214 return
215
216 # Check problems in the settings
217 problems = self._check_settings()
218 if problems:
219 self.unit.status = BlockedStatus(problems)
220 return
221
222 self.unit.status = BlockedStatus("Assembling pod spec")
223 image_details = self._make_pod_image_details()
224 ports = self._make_pod_ports()
225 env_config = self._make_pod_envconfig()
226 ingress_resources = self._make_pod_ingress_resources()
227
228 pod_spec = {
229 "version": 3,
230 "containers": [
231 {
232 "name": self.framework.model.app.name,
233 "imageDetails": image_details,
234 "ports": ports,
235 "envConfig": env_config,
236 }
237 ],
238 "kubernetesResources": {"ingressResources": ingress_resources or []},
239 }
240 self.model.pod.set_spec(pod_spec)
241 self.unit.status = ActiveStatus()
242
243
244 if __name__ == "__main__":
245 main(KeystoneCharm)