Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#!/usr/bin/env python3 

2# Copyright 2020 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 

23from ipaddress import ip_network 

24from typing import Any, Callable, Dict, List, NoReturn 

25from urllib.parse import urlparse 

26 

27 

28def _validate_max_file_size(max_file_size: int, site_url: str) -> bool: 

29 """Validate max_file_size. 

30 

31 Args: 

32 max_file_size (int): maximum file size allowed. 

33 site_url (str): endpoint url. 

34 

35 Returns: 

36 bool: True if valid, false otherwise. 

37 """ 

38 if not site_url: 

39 return True 

40 

41 parsed = urlparse(site_url) 

42 

43 if not parsed.scheme.startswith("http"): 

44 return True 

45 

46 if max_file_size is None: 

47 return False 

48 

49 return max_file_size >= 0 

50 

51 

52def _validate_ip_network(network: str) -> bool: 

53 """Validate IP network. 

54 

55 Args: 

56 network (str): IP network range. 

57 

58 Returns: 

59 bool: True if valid, false otherwise. 

60 """ 

61 if not network: 

62 return True 

63 

64 try: 

65 ip_network(network) 

66 except ValueError: 

67 return False 

68 

69 return True 

70 

71 

72def _validate_keystone_config(keystone: bool, value: Any, validator: Callable) -> bool: 

73 """Validate keystone configurations. 

74 

75 Args: 

76 keystone (bool): is keystone enabled, true if so, false otherwise. 

77 value (Any): value to be validated. 

78 validator (Callable): function to validate configuration. 

79 

80 Returns: 

81 bool: true if valid, false otherwise. 

82 """ 

83 if not keystone: 

84 return True 

85 

86 return validator(value) 

87 

88 

89def _validate_data( 

90 config_data: Dict[str, Any], relation_data: Dict[str, Any], keystone: bool 

91) -> NoReturn: 

92 """Validate input data. 

93 

94 Args: 

95 config_data (Dict[str, Any]): configuration data. 

96 relation_data (Dict[str, Any]): relation data. 

97 keystone (bool): is keystone to be used. 

98 """ 

99 config_validators = { 

100 "enable_test": lambda value, _: isinstance(value, bool), 

101 "database_commonkey": lambda value, _: ( 

102 isinstance(value, str) and len(value) > 1 

103 ), 

104 "log_level": lambda value, _: ( 

105 isinstance(value, str) and value in ("INFO", "DEBUG") 

106 ), 

107 "auth_backend": lambda value, _: ( 

108 isinstance(value, str) and (value == "internal" or value == "keystone") 

109 ), 

110 "site_url": lambda value, _: isinstance(value, str) 

111 if value is not None 

112 else True, 

113 "max_file_size": lambda value, values: _validate_max_file_size( 

114 value, values.get("site_url") 

115 ), 

116 "ingress_whitelist_source_range": lambda value, _: _validate_ip_network(value), 

117 "tls_secret_name": lambda value, _: isinstance(value, str) 

118 if value is not None 

119 else True, 

120 } 

121 relation_validators = { 

122 "message_host": lambda value, _: isinstance(value, str), 

123 "message_port": lambda value, _: isinstance(value, int) and value > 0, 

124 "database_uri": lambda value, _: ( 

125 isinstance(value, str) and value.startswith("mongodb://") 

126 ), 

127 "prometheus_host": lambda value, _: isinstance(value, str), 

128 "prometheus_port": lambda value, _: isinstance(value, int) and value > 0, 

129 "keystone_host": lambda value, _: _validate_keystone_config( 

130 keystone, value, lambda x: isinstance(x, str) and len(x) > 0 

131 ), 

132 "keystone_port": lambda value, _: _validate_keystone_config( 

133 keystone, value, lambda x: isinstance(x, int) and x > 0 

134 ), 

135 "keystone_user_domain_name": lambda value, _: _validate_keystone_config( 

136 keystone, value, lambda x: isinstance(x, str) and len(x) > 0 

137 ), 

138 "keystone_project_domain_name": lambda value, _: _validate_keystone_config( 

139 keystone, value, lambda x: isinstance(x, str) and len(x) > 0 

140 ), 

141 "keystone_username": lambda value, _: _validate_keystone_config( 

142 keystone, value, lambda x: isinstance(x, str) and len(x) > 0 

143 ), 

144 "keystone_password": lambda value, _: _validate_keystone_config( 

145 keystone, value, lambda x: isinstance(x, str) and len(x) > 0 

146 ), 

147 "keystone_service": lambda value, _: _validate_keystone_config( 

148 keystone, value, lambda x: isinstance(x, str) and len(x) > 0 

149 ), 

150 } 

151 problems = [] 

152 

153 for key, validator in config_validators.items(): 

154 valid = validator(config_data.get(key), config_data) 

155 

156 if not valid: 

157 problems.append(key) 

158 

159 for key, validator in relation_validators.items(): 

160 valid = validator(relation_data.get(key), relation_data) 

161 

162 if not valid: 

163 problems.append(key) 

164 

165 if len(problems) > 0: 

166 raise ValueError("Errors found in: {}".format(", ".join(problems))) 

167 

168 

169def _make_pod_ports(port: int) -> List[Dict[str, Any]]: 

170 """Generate pod ports details. 

171 

172 Args: 

173 port (int): port to expose. 

174 

175 Returns: 

176 List[Dict[str, Any]]: pod port details. 

177 """ 

178 return [{"name": "nbi", "containerPort": port, "protocol": "TCP"}] 

179 

180 

181def _make_pod_envconfig( 

182 config: Dict[str, Any], relation_state: Dict[str, Any] 

183) -> Dict[str, Any]: 

184 """Generate pod environment configuration. 

185 

186 Args: 

187 config (Dict[str, Any]): configuration information. 

188 relation_state (Dict[str, Any]): relation state information. 

189 

190 Returns: 

191 Dict[str, Any]: pod environment configuration. 

192 """ 

193 envconfig = { 

194 # General configuration 

195 "ALLOW_ANONYMOUS_LOGIN": "yes", 

196 "OSMNBI_SERVER_ENABLE_TEST": config["enable_test"], 

197 "OSMNBI_STATIC_DIR": "/app/osm_nbi/html_public", 

198 # Kafka configuration 

199 "OSMNBI_MESSAGE_HOST": relation_state["message_host"], 

200 "OSMNBI_MESSAGE_DRIVER": "kafka", 

201 "OSMNBI_MESSAGE_PORT": relation_state["message_port"], 

202 # Database configuration 

203 "OSMNBI_DATABASE_DRIVER": "mongo", 

204 "OSMNBI_DATABASE_URI": relation_state["database_uri"], 

205 "OSMNBI_DATABASE_COMMONKEY": config["database_commonkey"], 

206 # Storage configuration 

207 "OSMNBI_STORAGE_DRIVER": "mongo", 

208 "OSMNBI_STORAGE_PATH": "/app/storage", 

209 "OSMNBI_STORAGE_COLLECTION": "files", 

210 "OSMNBI_STORAGE_URI": relation_state["database_uri"], 

211 # Prometheus configuration 

212 "OSMNBI_PROMETHEUS_HOST": relation_state["prometheus_host"], 

213 "OSMNBI_PROMETHEUS_PORT": relation_state["prometheus_port"], 

214 # Log configuration 

215 "OSMNBI_LOG_LEVEL": config["log_level"], 

216 } 

217 

218 if config["auth_backend"] == "internal": 

219 envconfig["OSMNBI_AUTHENTICATION_BACKEND"] = "internal" 

220 elif config["auth_backend"] == "keystone": 

221 envconfig.update( 

222 { 

223 "OSMNBI_AUTHENTICATION_BACKEND": "keystone", 

224 "OSMNBI_AUTHENTICATION_AUTH_URL": relation_state["keystone_host"], 

225 "OSMNBI_AUTHENTICATION_AUTH_PORT": relation_state["keystone_port"], 

226 "OSMNBI_AUTHENTICATION_USER_DOMAIN_NAME": relation_state[ 

227 "keystone_user_domain_name" 

228 ], 

229 "OSMNBI_AUTHENTICATION_PROJECT_DOMAIN_NAME": relation_state[ 

230 "keystone_project_domain_name" 

231 ], 

232 "OSMNBI_AUTHENTICATION_SERVICE_USERNAME": relation_state[ 

233 "keystone_username" 

234 ], 

235 "OSMNBI_AUTHENTICATION_SERVICE_PASSWORD": relation_state[ 

236 "keystone_password" 

237 ], 

238 "OSMNBI_AUTHENTICATION_SERVICE_PROJECT": relation_state[ 

239 "keystone_service" 

240 ], 

241 } 

242 ) 

243 else: 

244 raise ValueError("auth_backend needs to be either internal or keystone") 

245 

246 return envconfig 

247 

248 

249def _make_pod_ingress_resources( 

250 config: Dict[str, Any], app_name: str, port: int 

251) -> List[Dict[str, Any]]: 

252 """Generate pod ingress resources. 

253 

254 Args: 

255 config (Dict[str, Any]): configuration information. 

256 app_name (str): application name. 

257 port (int): port to expose. 

258 

259 Returns: 

260 List[Dict[str, Any]]: pod ingress resources. 

261 """ 

262 site_url = config.get("site_url") 

263 

264 if not site_url: 

265 return 

266 

267 parsed = urlparse(site_url) 

268 

269 if not parsed.scheme.startswith("http"): 

270 return 

271 

272 max_file_size = config["max_file_size"] 

273 ingress_whitelist_source_range = config["ingress_whitelist_source_range"] 

274 

275 annotations = { 

276 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format( 

277 str(max_file_size) + "m" if max_file_size > 0 else max_file_size 

278 ), 

279 "nginx.ingress.kubernetes.io/backend-protocol": "HTTPS", 

280 } 

281 

282 if ingress_whitelist_source_range: 

283 annotations[ 

284 "nginx.ingress.kubernetes.io/whitelist-source-range" 

285 ] = ingress_whitelist_source_range 

286 

287 ingress_spec_tls = None 

288 

289 if parsed.scheme == "https": 

290 ingress_spec_tls = [{"hosts": [parsed.hostname]}] 

291 tls_secret_name = config["tls_secret_name"] 

292 if tls_secret_name: 

293 ingress_spec_tls[0]["secretName"] = tls_secret_name 

294 else: 

295 annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false" 

296 

297 ingress = { 

298 "name": "{}-ingress".format(app_name), 

299 "annotations": annotations, 

300 "spec": { 

301 "rules": [ 

302 { 

303 "host": parsed.hostname, 

304 "http": { 

305 "paths": [ 

306 { 

307 "path": "/", 

308 "backend": { 

309 "serviceName": app_name, 

310 "servicePort": port, 

311 }, 

312 } 

313 ] 

314 }, 

315 } 

316 ] 

317 }, 

318 } 

319 if ingress_spec_tls: 

320 ingress["spec"]["tls"] = ingress_spec_tls 

321 

322 return [ingress] 

323 

324 

325def _make_startup_probe() -> Dict[str, Any]: 

326 """Generate startup probe. 

327 

328 Returns: 

329 Dict[str, Any]: startup probe. 

330 """ 

331 return { 

332 "exec": {"command": ["/usr/bin/pgrep python3"]}, 

333 "initialDelaySeconds": 60, 

334 "timeoutSeconds": 5, 

335 } 

336 

337 

338def _make_readiness_probe(port: int) -> Dict[str, Any]: 

339 """Generate readiness probe. 

340 

341 Args: 

342 port (int): [description] 

343 

344 Returns: 

345 Dict[str, Any]: readiness probe. 

346 """ 

347 return { 

348 "httpGet": { 

349 "path": "/osm/", 

350 "port": port, 

351 }, 

352 "initialDelaySeconds": 45, 

353 "timeoutSeconds": 5, 

354 } 

355 

356 

357def _make_liveness_probe(port: int) -> Dict[str, Any]: 

358 """Generate liveness probe. 

359 

360 Args: 

361 port (int): [description] 

362 

363 Returns: 

364 Dict[str, Any]: liveness probe. 

365 """ 

366 return { 

367 "httpGet": { 

368 "path": "/osm/", 

369 "port": port, 

370 }, 

371 "initialDelaySeconds": 45, 

372 "timeoutSeconds": 5, 

373 } 

374 

375 

376def make_pod_spec( 

377 image_info: Dict[str, str], 

378 config: Dict[str, Any], 

379 relation_state: Dict[str, Any], 

380 app_name: str = "nbi", 

381 port: int = 9999, 

382) -> Dict[str, Any]: 

383 """Generate the pod spec information. 

384 

385 Args: 

386 image_info (Dict[str, str]): Object provided by 

387 OCIImageResource("image").fetch(). 

388 config (Dict[str, Any]): Configuration information. 

389 relation_state (Dict[str, Any]): Relation state information. 

390 app_name (str, optional): Application name. Defaults to "nbi". 

391 port (int, optional): Port for the container. Defaults to 9999. 

392 

393 Returns: 

394 Dict[str, Any]: Pod spec dictionary for the charm. 

395 """ 

396 if not image_info: 

397 return None 

398 

399 _validate_data(config, relation_state, config.get("auth_backend") == "keystone") 

400 

401 ports = _make_pod_ports(port) 

402 env_config = _make_pod_envconfig(config, relation_state) 

403 ingress_resources = _make_pod_ingress_resources(config, app_name, port) 

404 

405 return { 

406 "version": 3, 

407 "containers": [ 

408 { 

409 "name": app_name, 

410 "imageDetails": image_info, 

411 "imagePullPolicy": "Always", 

412 "ports": ports, 

413 "envConfig": env_config, 

414 } 

415 ], 

416 "kubernetesResources": { 

417 "ingressResources": ingress_resources or [], 

418 }, 

419 }