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 

26import logging 

27from typing import NoReturn, Optional 

28 

29 

30from ops.main import main 

31from opslib.osm.charm import CharmedOsmBase, RelationsMissing 

32from opslib.osm.interfaces.http import HttpClient 

33from opslib.osm.interfaces.kafka import KafkaClient 

34from opslib.osm.interfaces.mongo import MongoClient 

35from opslib.osm.pod import ContainerV3Builder, PodSpecV3Builder 

36from opslib.osm.validator import ModelValidator, validator 

37 

38 

39logger = logging.getLogger(__name__) 

40 

41PORT = 9999 

42 

43 

44class ConfigModel(ModelValidator): 

45 vca_host: str 

46 vca_port: int 

47 vca_user: str 

48 vca_secret: str 

49 vca_pubkey: str 

50 vca_cacert: str 

51 vca_cloud: str 

52 vca_k8s_cloud: str 

53 database_commonkey: str 

54 log_level: str 

55 vca_apiproxy: Optional[str] 

56 # Model-config options 

57 vca_model_config_agent_metadata_url: Optional[str] 

58 vca_model_config_agent_stream: Optional[str] 

59 vca_model_config_apt_ftp_proxy: Optional[str] 

60 vca_model_config_apt_http_proxy: Optional[str] 

61 vca_model_config_apt_https_proxy: Optional[str] 

62 vca_model_config_apt_mirror: Optional[str] 

63 vca_model_config_apt_no_proxy: Optional[str] 

64 vca_model_config_automatically_retry_hooks: Optional[bool] 

65 vca_model_config_backup_dir: Optional[str] 

66 vca_model_config_cloudinit_userdata: Optional[str] 

67 vca_model_config_container_image_metadata_url: Optional[str] 

68 vca_model_config_container_image_stream: Optional[str] 

69 vca_model_config_container_inherit_properties: Optional[str] 

70 vca_model_config_container_networking_method: Optional[str] 

71 vca_model_config_default_series: Optional[str] 

72 vca_model_config_default_space: Optional[str] 

73 vca_model_config_development: Optional[bool] 

74 vca_model_config_disable_network_management: Optional[bool] 

75 vca_model_config_egress_subnets: Optional[str] 

76 vca_model_config_enable_os_refresh_update: Optional[bool] 

77 vca_model_config_enable_os_upgrade: Optional[bool] 

78 vca_model_config_fan_config: Optional[str] 

79 vca_model_config_firewall_mode: Optional[str] 

80 vca_model_config_ftp_proxy: Optional[str] 

81 vca_model_config_http_proxy: Optional[str] 

82 vca_model_config_https_proxy: Optional[str] 

83 vca_model_config_ignore_machine_addresses: Optional[bool] 

84 vca_model_config_image_metadata_url: Optional[str] 

85 vca_model_config_image_stream: Optional[str] 

86 vca_model_config_juju_ftp_proxy: Optional[str] 

87 vca_model_config_juju_http_proxy: Optional[str] 

88 vca_model_config_juju_https_proxy: Optional[str] 

89 vca_model_config_juju_no_proxy: Optional[str] 

90 vca_model_config_logforward_enabled: Optional[bool] 

91 vca_model_config_logging_config: Optional[str] 

92 vca_model_config_lxd_snap_channel: Optional[str] 

93 vca_model_config_max_action_results_age: Optional[str] 

94 vca_model_config_max_action_results_size: Optional[str] 

95 vca_model_config_max_status_history_age: Optional[str] 

96 vca_model_config_max_status_history_size: Optional[str] 

97 vca_model_config_net_bond_reconfigure_delay: Optional[str] 

98 vca_model_config_no_proxy: Optional[str] 

99 vca_model_config_provisioner_harvest_mode: Optional[str] 

100 vca_model_config_proxy_ssh: Optional[bool] 

101 vca_model_config_snap_http_proxy: Optional[str] 

102 vca_model_config_snap_https_proxy: Optional[str] 

103 vca_model_config_snap_store_assertions: Optional[str] 

104 vca_model_config_snap_store_proxy: Optional[str] 

105 vca_model_config_snap_store_proxy_url: Optional[str] 

106 vca_model_config_ssl_hostname_verification: Optional[bool] 

107 vca_model_config_test_mode: Optional[bool] 

108 vca_model_config_transmit_vendor_metrics: Optional[bool] 

109 vca_model_config_update_status_hook_interval: Optional[str] 

110 

111 @validator("log_level") 

112 def validate_log_level(cls, v): 

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

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

115 return v 

116 

117 

118class LcmCharm(CharmedOsmBase): 

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

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

121 

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

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

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

125 

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

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

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

129 

130 self.ro_client = HttpClient(self, "ro") 

131 self.framework.observe(self.on["ro"].relation_changed, self.configure_pod) 

132 self.framework.observe(self.on["ro"].relation_broken, self.configure_pod) 

133 

134 def _check_missing_dependencies(self, config: ConfigModel): 

135 missing_relations = [] 

136 

137 if self.kafka_client.is_missing_data_in_unit(): 

138 missing_relations.append("kafka") 

139 if self.mongodb_client.is_missing_data_in_unit(): 

140 missing_relations.append("mongodb") 

141 if self.ro_client.is_missing_data_in_app(): 

142 missing_relations.append("ro") 

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 Container 

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

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

157 container_builder.add_envs( 

158 { 

159 # General configuration 

160 "ALLOW_ANONYMOUS_LOGIN": "yes", 

161 "OSMLCM_GLOBAL_LOGLEVEL": config.log_level, 

162 # RO configuration 

163 "OSMLCM_RO_HOST": self.ro_client.host, 

164 "OSMLCM_RO_PORT": self.ro_client.port, 

165 "OSMLCM_RO_TENANT": "osm", 

166 # Kafka configuration 

167 "OSMLCM_MESSAGE_DRIVER": "kafka", 

168 "OSMLCM_MESSAGE_HOST": self.kafka_client.host, 

169 "OSMLCM_MESSAGE_PORT": self.kafka_client.port, 

170 # Database configuration 

171 "OSMLCM_DATABASE_DRIVER": "mongo", 

172 "OSMLCM_DATABASE_URI": self.mongodb_client.connection_string, 

173 "OSMLCM_DATABASE_COMMONKEY": config.database_commonkey, 

174 # Storage configuration 

175 "OSMLCM_STORAGE_DRIVER": "mongo", 

176 "OSMLCM_STORAGE_PATH": "/app/storage", 

177 "OSMLCM_STORAGE_COLLECTION": "files", 

178 "OSMLCM_STORAGE_URI": self.mongodb_client.connection_string, 

179 # VCA configuration 

180 "OSMLCM_VCA_HOST": config.vca_host, 

181 "OSMLCM_VCA_PORT": config.vca_port, 

182 "OSMLCM_VCA_USER": config.vca_user, 

183 "OSMLCM_VCA_PUBKEY": config.vca_pubkey, 

184 "OSMLCM_VCA_SECRET": config.vca_secret, 

185 "OSMLCM_VCA_CACERT": config.vca_cacert, 

186 "OSMLCM_VCA_CLOUD": config.vca_cloud, 

187 "OSMLCM_VCA_K8S_CLOUD": config.vca_k8s_cloud, 

188 } 

189 ) 

190 if config.vca_apiproxy: 

191 container_builder.add_env("OSMLCM_VCA_APIPROXY", config.vca_apiproxy) 

192 

193 model_config_envs = { 

194 f"OSMLCM_{k.upper()}": v 

195 for k, v in self.config.items() 

196 if k.startswith("vca_model_config") 

197 } 

198 if model_config_envs: 

199 container_builder.add_envs(model_config_envs) 

200 container = container_builder.build() 

201 # Add container to pod spec 

202 pod_spec_builder.add_container(container) 

203 return pod_spec_builder.build() 

204 

205 

206if __name__ == "__main__": 

207 main(LcmCharm) 

208 

209 

210# class ConfigurePodEvent(EventBase): 

211# """Configure Pod event""" 

212 

213# pass 

214 

215 

216# class LcmEvents(CharmEvents): 

217# """LCM Events""" 

218 

219# configure_pod = EventSource(ConfigurePodEvent) 

220 

221 

222# class LcmCharm(CharmBase): 

223# """LCM Charm.""" 

224 

225# state = StoredState() 

226# on = LcmEvents() 

227 

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

229# """LCM Charm constructor.""" 

230# super().__init__(*args) 

231 

232# # Internal state initialization 

233# self.state.set_default(pod_spec=None) 

234 

235# # Message bus data initialization 

236# self.state.set_default(message_host=None) 

237# self.state.set_default(message_port=None) 

238 

239# # Database data initialization 

240# self.state.set_default(database_uri=None) 

241 

242# # RO data initialization 

243# self.state.set_default(ro_host=None) 

244# self.state.set_default(ro_port=None) 

245 

246# self.port = LCM_PORT 

247# self.image = OCIImageResource(self, "image") 

248 

249# # Registering regular events 

250# self.framework.observe(self.on.start, self.configure_pod) 

251# self.framework.observe(self.on.config_changed, self.configure_pod) 

252# self.framework.observe(self.on.upgrade_charm, self.configure_pod) 

253 

254# # Registering custom internal events 

255# self.framework.observe(self.on.configure_pod, self.configure_pod) 

256 

257# # Registering required relation events 

258# self.framework.observe( 

259# self.on.kafka_relation_changed, self._on_kafka_relation_changed 

260# ) 

261# self.framework.observe( 

262# self.on.mongodb_relation_changed, self._on_mongodb_relation_changed 

263# ) 

264# self.framework.observe( 

265# self.on.ro_relation_changed, self._on_ro_relation_changed 

266# ) 

267 

268# # Registering required relation broken events 

269# self.framework.observe( 

270# self.on.kafka_relation_broken, self._on_kafka_relation_broken 

271# ) 

272# self.framework.observe( 

273# self.on.mongodb_relation_broken, self._on_mongodb_relation_broken 

274# ) 

275# self.framework.observe( 

276# self.on.ro_relation_broken, self._on_ro_relation_broken 

277# ) 

278 

279# def _on_kafka_relation_changed(self, event: EventBase) -> NoReturn: 

280# """Reads information about the kafka relation. 

281 

282# Args: 

283# event (EventBase): Kafka relation event. 

284# """ 

285# message_host = event.relation.data[event.unit].get("host") 

286# message_port = event.relation.data[event.unit].get("port") 

287 

288# if ( 

289# message_host 

290# and message_port 

291# and ( 

292# self.state.message_host != message_host 

293# or self.state.message_port != message_port 

294# ) 

295# ): 

296# self.state.message_host = message_host 

297# self.state.message_port = message_port 

298# self.on.configure_pod.emit() 

299 

300# def _on_kafka_relation_broken(self, event: EventBase) -> NoReturn: 

301# """Clears data from kafka relation. 

302 

303# Args: 

304# event (EventBase): Kafka relation event. 

305# """ 

306# self.state.message_host = None 

307# self.state.message_port = None 

308# self.on.configure_pod.emit() 

309 

310# def _on_mongodb_relation_changed(self, event: EventBase) -> NoReturn: 

311# """Reads information about the DB relation. 

312 

313# Args: 

314# event (EventBase): DB relation event. 

315# """ 

316# database_uri = event.relation.data[event.unit].get("connection_string") 

317 

318# if database_uri and self.state.database_uri != database_uri: 

319# self.state.database_uri = database_uri 

320# self.on.configure_pod.emit() 

321 

322# def _on_mongodb_relation_broken(self, event: EventBase) -> NoReturn: 

323# """Clears data from mongodb relation. 

324 

325# Args: 

326# event (EventBase): DB relation event. 

327# """ 

328# self.state.database_uri = None 

329# self.on.configure_pod.emit() 

330 

331# def _on_ro_relation_changed(self, event: EventBase) -> NoReturn: 

332# """Reads information about the RO relation. 

333 

334# Args: 

335# event (EventBase): Keystone relation event. 

336# """ 

337# ro_host = event.relation.data[event.unit].get("host") 

338# ro_port = event.relation.data[event.unit].get("port") 

339 

340# if ( 

341# ro_host 

342# and ro_port 

343# and (self.state.ro_host != ro_host or self.state.ro_port != ro_port) 

344# ): 

345# self.state.ro_host = ro_host 

346# self.state.ro_port = ro_port 

347# self.on.configure_pod.emit() 

348 

349# def _on_ro_relation_broken(self, event: EventBase) -> NoReturn: 

350# """Clears data from ro relation. 

351 

352# Args: 

353# event (EventBase): Keystone relation event. 

354# """ 

355# self.state.ro_host = None 

356# self.state.ro_port = None 

357# self.on.configure_pod.emit() 

358 

359# def _missing_relations(self) -> str: 

360# """Checks if there missing relations. 

361 

362# Returns: 

363# str: string with missing relations 

364# """ 

365# data_status = { 

366# "kafka": self.state.message_host, 

367# "mongodb": self.state.database_uri, 

368# "ro": self.state.ro_host, 

369# } 

370 

371# missing_relations = [k for k, v in data_status.items() if not v] 

372 

373# return ", ".join(missing_relations) 

374 

375# @property 

376# def relation_state(self) -> Dict[str, Any]: 

377# """Collects relation state configuration for pod spec assembly. 

378 

379# Returns: 

380# Dict[str, Any]: relation state information. 

381# """ 

382# relation_state = { 

383# "message_host": self.state.message_host, 

384# "message_port": self.state.message_port, 

385# "database_uri": self.state.database_uri, 

386# "ro_host": self.state.ro_host, 

387# "ro_port": self.state.ro_port, 

388# } 

389 

390# return relation_state 

391 

392# def configure_pod(self, event: EventBase) -> NoReturn: 

393# """Assemble the pod spec and apply it, if possible. 

394 

395# Args: 

396# event (EventBase): Hook or Relation event that started the 

397# function. 

398# """ 

399# if missing := self._missing_relations(): 

400# self.unit.status = BlockedStatus( 

401# "Waiting for {0} relation{1}".format( 

402# missing, "s" if "," in missing else "" 

403# ) 

404# ) 

405# return 

406 

407# if not self.unit.is_leader(): 

408# self.unit.status = ActiveStatus("ready") 

409# return 

410 

411# self.unit.status = MaintenanceStatus("Assembling pod spec") 

412 

413# # Fetch image information 

414# try: 

415# self.unit.status = MaintenanceStatus("Fetching image information") 

416# image_info = self.image.fetch() 

417# except OCIImageResourceError: 

418# self.unit.status = BlockedStatus("Error fetching image information") 

419# return 

420 

421# try: 

422# pod_spec = make_pod_spec( 

423# image_info, 

424# self.model.config, 

425# self.relation_state, 

426# self.model.app.name, 

427# self.port, 

428# ) 

429# except ValueError as exc: 

430# logger.exception("Config/Relation data validation error") 

431# self.unit.status = BlockedStatus(str(exc)) 

432# return 

433 

434# if self.state.pod_spec != pod_spec: 

435# self.model.pod.set_spec(pod_spec) 

436# self.state.pod_spec = pod_spec 

437 

438# self.unit.status = ActiveStatus("ready") 

439 

440 

441# if __name__ == "__main__": 

442# main(LcmCharm)