Adding LDAP support for Keystone charm
[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 envconfig = {
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 if config.get("ldap_enabled"):
143 envconfig["LDAP_AUTHENTICATION_DOMAIN_NAME"] = config[
144 "ldap_authentication_domain_name"
145 ]
146 envconfig["LDAP_URL"] = config["ldap_url"]
147 envconfig["LDAP_USER_OBJECTCLASS"] = config["ldap_user_objectclass"]
148 envconfig["LDAP_USER_ID_ATTRIBUTE"] = config["ldap_user_id_attribute"]
149 envconfig["LDAP_USER_NAME_ATTRIBUTE"] = config["ldap_user_name_attribute"]
150 envconfig["LDAP_USER_PASS_ATTRIBUTE"] = config["ldap_user_pass_attribute"]
151 envconfig["LDAP_USER_ENABLED_MASK"] = config["ldap_user_enabled_mask"]
152 envconfig["LDAP_USER_ENABLED_DEFAULT"] = config["ldap_user_enabled_default"]
153 envconfig["LDAP_USER_ENABLED_INVERT"] = config["ldap_user_enabled_invert"]
154
155 if config["ldap_bind_user"]:
156 envconfig["LDAP_BIND_USER"] = config["ldap_bind_user"]
157
158 if config["ldap_bind_password"]:
159 envconfig["LDAP_BIND_PASSWORD"] = config["ldap_bind_password"]
160
161 if config["ldap_user_tree_dn"]:
162 envconfig["LDAP_USER_TREE_DN"] = config["ldap_user_tree_dn"]
163
164 if config["ldap_user_filter"]:
165 envconfig["LDAP_USER_FILTER"] = config["ldap_user_filter"]
166
167 if config["ldap_user_enabled_attribute"]:
168 envconfig["LDAP_USER_ENABLED_ATTRIBUTE"] = config[
169 "ldap_user_enabled_attribute"
170 ]
171
172 if config["ldap_use_starttls"]:
173 envconfig["LDAP_USE_STARTTLS"] = config["ldap_use_starttls"]
174 envconfig["LDAP_TLS_CACERT_BASE64"] = config["ldap_tls_cacert_base64"]
175 envconfig["LDAP_TLS_REQ_CERT"] = config["ldap_tls_req_cert"]
176
177 return envconfig
178
179 def _make_pod_ingress_resources(self):
180 site_url = self.model.config["site_url"]
181
182 if not site_url:
183 return
184
185 parsed = urlparse(site_url)
186
187 if not parsed.scheme.startswith("http"):
188 return
189
190 max_file_size = self.model.config["max_file_size"]
191 ingress_whitelist_source_range = self.model.config[
192 "ingress_whitelist_source_range"
193 ]
194
195 annotations = {
196 "nginx.ingress.kubernetes.io/proxy-body-size": "{}m".format(max_file_size)
197 }
198
199 if ingress_whitelist_source_range:
200 annotations[
201 "nginx.ingress.kubernetes.io/whitelist-source-range"
202 ] = ingress_whitelist_source_range
203
204 ingress_spec_tls = None
205
206 if parsed.scheme == "https":
207 ingress_spec_tls = [{"hosts": [parsed.hostname]}]
208 tls_secret_name = self.model.config["tls_secret_name"]
209 if tls_secret_name:
210 ingress_spec_tls[0]["secretName"] = tls_secret_name
211 else:
212 annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
213
214 ingress = {
215 "name": "{}-ingress".format(self.app.name),
216 "annotations": annotations,
217 "spec": {
218 "rules": [
219 {
220 "host": parsed.hostname,
221 "http": {
222 "paths": [
223 {
224 "path": "/",
225 "backend": {
226 "serviceName": self.app.name,
227 "servicePort": KEYSTONE_PORT,
228 },
229 }
230 ]
231 },
232 }
233 ],
234 },
235 }
236 if ingress_spec_tls:
237 ingress["spec"]["tls"] = ingress_spec_tls
238
239 return [ingress]
240
241 def configure_pod(self, event):
242 """Assemble the pod spec and apply it, if possible."""
243
244 if not self.state.db_host:
245 self.unit.status = WaitingStatus("Waiting for database relation")
246 event.defer()
247 return
248
249 if not self.unit.is_leader():
250 self.unit.status = ActiveStatus()
251 return
252
253 # Check problems in the settings
254 problems = self._check_settings()
255 if problems:
256 self.unit.status = BlockedStatus(problems)
257 return
258
259 self.unit.status = BlockedStatus("Assembling pod spec")
260 image_details = self._make_pod_image_details()
261 ports = self._make_pod_ports()
262 env_config = self._make_pod_envconfig()
263 ingress_resources = self._make_pod_ingress_resources()
264
265 pod_spec = {
266 "version": 3,
267 "containers": [
268 {
269 "name": self.framework.model.app.name,
270 "imageDetails": image_details,
271 "ports": ports,
272 "envConfig": env_config,
273 }
274 ],
275 "kubernetesResources": {"ingressResources": ingress_resources or []},
276 }
277 self.model.pod.set_spec(pod_spec)
278 self.unit.status = ActiveStatus()
279
280
281 if __name__ == "__main__":
282 main(KeystoneCharm)