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 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 

26from ipaddress import ip_network 

27import logging 

28from typing import NoReturn, Optional 

29from urllib.parse import urlparse 

30 

31 

32from ops.main import main 

33from opslib.osm.charm import CharmedOsmBase, RelationsMissing 

34from opslib.osm.interfaces.http import HttpServer 

35from opslib.osm.interfaces.kafka import KafkaClient 

36from opslib.osm.interfaces.keystone import KeystoneClient 

37from opslib.osm.interfaces.mongo import MongoClient 

38from opslib.osm.interfaces.prometheus import PrometheusClient 

39from opslib.osm.pod import ( 

40 ContainerV3Builder, 

41 IngressResourceV3Builder, 

42 PodSpecV3Builder, 

43) 

44from opslib.osm.validator import ModelValidator, validator 

45 

46 

47logger = logging.getLogger(__name__) 

48 

49PORT = 9999 

50 

51 

52class ConfigModel(ModelValidator): 

53 enable_test: bool 

54 auth_backend: str 

55 database_commonkey: str 

56 log_level: str 

57 max_file_size: int 

58 site_url: Optional[str] 

59 ingress_whitelist_source_range: Optional[str] 

60 tls_secret_name: Optional[str] 

61 

62 @validator("auth_backend") 

63 def validate_auth_backend(cls, v): 

64 if v not in {"internal", "keystone"}: 

65 raise ValueError("value must be 'internal' or 'keystone'") 

66 return v 

67 

68 @validator("log_level") 

69 def validate_log_level(cls, v): 

70 if v not in {"INFO", "DEBUG"}: 

71 raise ValueError("value must be INFO or DEBUG") 

72 return v 

73 

74 @validator("max_file_size") 

75 def validate_max_file_size(cls, v): 

76 if v < 0: 

77 raise ValueError("value must be equal or greater than 0") 

78 return v 

79 

80 @validator("site_url") 

81 def validate_site_url(cls, v): 

82 if v: 

83 parsed = urlparse(v) 

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

85 raise ValueError("value must start with http") 

86 return v 

87 

88 @validator("ingress_whitelist_source_range") 

89 def validate_ingress_whitelist_source_range(cls, v): 

90 if v: 

91 ip_network(v) 

92 return v 

93 

94 

95class NbiCharm(CharmedOsmBase): 

96 def __init__(self, *args) -> NoReturn: 

97 super().__init__(*args, oci_image="image") 

98 

99 self.kafka_client = KafkaClient(self, "kafka") 

100 self.framework.observe(self.on["kafka"].relation_changed, self.configure_pod) 

101 self.framework.observe(self.on["kafka"].relation_broken, self.configure_pod) 

102 

103 self.mongodb_client = MongoClient(self, "mongodb") 

104 self.framework.observe(self.on["mongodb"].relation_changed, self.configure_pod) 

105 self.framework.observe(self.on["mongodb"].relation_broken, self.configure_pod) 

106 

107 self.prometheus_client = PrometheusClient(self, "prometheus") 

108 self.framework.observe( 

109 self.on["prometheus"].relation_changed, self.configure_pod 

110 ) 

111 self.framework.observe( 

112 self.on["prometheus"].relation_broken, self.configure_pod 

113 ) 

114 

115 self.keystone_client = KeystoneClient(self, "keystone") 

116 self.framework.observe(self.on["keystone"].relation_changed, self.configure_pod) 

117 self.framework.observe(self.on["keystone"].relation_broken, self.configure_pod) 

118 

119 self.http_server = HttpServer(self, "nbi") 

120 self.framework.observe(self.on["nbi"].relation_joined, self._publish_nbi_info) 

121 

122 def _publish_nbi_info(self, event): 

123 """Publishes NBI information. 

124 

125 Args: 

126 event (EventBase): RO relation event. 

127 """ 

128 if self.unit.is_leader(): 

129 self.http_server.publish_info(self.app.name, PORT) 

130 

131 def _check_missing_dependencies(self, config: ConfigModel): 

132 missing_relations = [] 

133 

134 if self.kafka_client.is_missing_data_in_unit(): 

135 missing_relations.append("kafka") 

136 if self.mongodb_client.is_missing_data_in_unit(): 

137 missing_relations.append("mongodb") 

138 if self.prometheus_client.is_missing_data_in_app(): 

139 missing_relations.append("prometheus") 

140 if config.auth_backend == "keystone": 

141 if self.keystone_client.is_missing_data_in_app(): 

142 missing_relations.append("keystone") 

143 

144 if missing_relations: 

145 raise RelationsMissing(missing_relations) 

146 

147 def build_pod_spec(self, image_info): 

148 # Validate config 

149 config = ConfigModel(**dict(self.config)) 

150 # Check relations 

151 self._check_missing_dependencies(config) 

152 # Create Builder for the PodSpec 

153 pod_spec_builder = PodSpecV3Builder() 

154 # Build Init Container 

155 pod_spec_builder.add_init_container( 

156 { 

157 "name": "init-check", 

158 "image": "alpine:latest", 

159 "command": [ 

160 "sh", 

161 "-c", 

162 f"until (nc -zvw1 {self.kafka_client.host} {self.kafka_client.port} ); do sleep 3; done; exit 0", 

163 ], 

164 } 

165 ) 

166 # Build Container 

167 container_builder = ContainerV3Builder(self.app.name, image_info) 

168 container_builder.add_port(name=self.app.name, port=PORT) 

169 container_builder.add_tcpsocket_readiness_probe( 

170 PORT, 

171 initial_delay_seconds=5, 

172 timeout_seconds=5, 

173 ) 

174 container_builder.add_tcpsocket_liveness_probe( 

175 PORT, 

176 initial_delay_seconds=45, 

177 timeout_seconds=10, 

178 ) 

179 container_builder.add_envs( 

180 { 

181 # General configuration 

182 "ALLOW_ANONYMOUS_LOGIN": "yes", 

183 "OSMNBI_SERVER_ENABLE_TEST": config.enable_test, 

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

185 # Kafka configuration 

186 "OSMNBI_MESSAGE_HOST": self.kafka_client.host, 

187 "OSMNBI_MESSAGE_DRIVER": "kafka", 

188 "OSMNBI_MESSAGE_PORT": self.kafka_client.port, 

189 # Database configuration 

190 "OSMNBI_DATABASE_DRIVER": "mongo", 

191 "OSMNBI_DATABASE_URI": self.mongodb_client.connection_string, 

192 "OSMNBI_DATABASE_COMMONKEY": config.database_commonkey, 

193 # Storage configuration 

194 "OSMNBI_STORAGE_DRIVER": "mongo", 

195 "OSMNBI_STORAGE_PATH": "/app/storage", 

196 "OSMNBI_STORAGE_COLLECTION": "files", 

197 "OSMNBI_STORAGE_URI": self.mongodb_client.connection_string, 

198 # Prometheus configuration 

199 "OSMNBI_PROMETHEUS_HOST": self.prometheus_client.hostname, 

200 "OSMNBI_PROMETHEUS_PORT": self.prometheus_client.port, 

201 # Log configuration 

202 "OSMNBI_LOG_LEVEL": config.log_level, 

203 } 

204 ) 

205 if config.auth_backend == "internal": 

206 container_builder.add_env("OSMNBI_AUTHENTICATION_BACKEND", "internal") 

207 elif config.auth_backend == "keystone": 

208 container_builder.add_envs( 

209 { 

210 "OSMNBI_AUTHENTICATION_BACKEND": "keystone", 

211 "OSMNBI_AUTHENTICATION_AUTH_URL": self.keystone_client.host, 

212 "OSMNBI_AUTHENTICATION_AUTH_PORT": self.keystone_client.port, 

213 "OSMNBI_AUTHENTICATION_USER_DOMAIN_NAME": self.keystone_client.user_domain_name, 

214 "OSMNBI_AUTHENTICATION_PROJECT_DOMAIN_NAME": self.keystone_client.project_domain_name, 

215 "OSMNBI_AUTHENTICATION_SERVICE_USERNAME": self.keystone_client.username, 

216 "OSMNBI_AUTHENTICATION_SERVICE_PASSWORD": self.keystone_client.password, 

217 "OSMNBI_AUTHENTICATION_SERVICE_PROJECT": self.keystone_client.service, 

218 } 

219 ) 

220 container = container_builder.build() 

221 # Add container to pod spec 

222 pod_spec_builder.add_container(container) 

223 # Add ingress resources to pod spec if site url exists 

224 if config.site_url: 

225 parsed = urlparse(config.site_url) 

226 annotations = { 

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

228 str(config.max_file_size) + "m" 

229 if config.max_file_size > 0 

230 else config.max_file_size 

231 ), 

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

233 } 

234 ingress_resource_builder = IngressResourceV3Builder( 

235 f"{self.app.name}-ingress", annotations 

236 ) 

237 

238 if config.ingress_whitelist_source_range: 

239 annotations[ 

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

241 ] = config.ingress_whitelist_source_range 

242 

243 if parsed.scheme == "https": 

244 ingress_resource_builder.add_tls( 

245 [parsed.hostname], config.tls_secret_name 

246 ) 

247 else: 

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

249 

250 ingress_resource_builder.add_rule(parsed.hostname, self.app.name, PORT) 

251 ingress_resource = ingress_resource_builder.build() 

252 pod_spec_builder.add_ingress_resource(ingress_resource) 

253 logger.debug(pod_spec_builder.build()) 

254 return pod_spec_builder.build() 

255 

256 

257if __name__ == "__main__": 

258 main(NbiCharm)