944d8cecd03bf5dc77c2a682c68991d7f1997ee7
[osm/devops.git] / installers / charm / ng-ui / 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"); 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 import logging
24 from typing import Any, Dict, NoReturn
25 from pydantic import ValidationError
26
27 from ops.charm import CharmBase, CharmEvents
28 from ops.framework import EventBase, EventSource, StoredState
29 from ops.main import main
30 from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus
31 from oci_image import OCIImageResource, OCIImageResourceError
32
33 from pod_spec import make_pod_spec
34
35 logger = logging.getLogger(__name__)
36
37 NGUI_PORT = 80
38
39
40 class ConfigurePodEvent(EventBase):
41 """Configure Pod event"""
42
43 pass
44
45
46 class NgUiEvents(CharmEvents):
47 """NGUI Events"""
48
49 configure_pod = EventSource(ConfigurePodEvent)
50
51
52 class NgUiCharm(CharmBase):
53 """NGUI Charm."""
54
55 state = StoredState()
56 on = NgUiEvents()
57
58 def __init__(self, *args) -> NoReturn:
59 """NGUI Charm constructor."""
60 super().__init__(*args)
61
62 # Internal state initialization
63 self.state.set_default(pod_spec=None)
64
65 # North bound interface initialization
66 self.state.set_default(nbi_host=None)
67 self.state.set_default(nbi_port=None)
68
69 self.http_port = NGUI_PORT
70 self.image = OCIImageResource(self, "image")
71
72 # Registering regular events
73 self.framework.observe(self.on.start, self.configure_pod)
74 self.framework.observe(self.on.config_changed, self.configure_pod)
75 # self.framework.observe(self.on.upgrade_charm, self.configure_pod)
76
77 # Registering custom internal events
78 self.framework.observe(self.on.configure_pod, self.configure_pod)
79
80 # Registering required relation changed events
81 self.framework.observe(
82 self.on.nbi_relation_changed, self._on_nbi_relation_changed
83 )
84
85 # Registering required relation departed events
86 self.framework.observe(
87 self.on.nbi_relation_departed, self._on_nbi_relation_departed
88 )
89
90 def _on_nbi_relation_changed(self, event: EventBase) -> NoReturn:
91 """Reads information about the nbi relation.
92
93 Args:
94 event (EventBase): NBI relation event.
95 """
96 if event.unit not in event.relation.data:
97 return
98 relation_data = event.relation.data[event.unit]
99 nbi_host = relation_data.get("host")
100 nbi_port = relation_data.get("port")
101
102 if (
103 nbi_host
104 and nbi_port
105 and (self.state.nbi_host != nbi_host or self.state.nbi_port != nbi_port)
106 ):
107 self.state.nbi_host = nbi_host
108 self.state.nbi_port = nbi_port
109 self.on.configure_pod.emit()
110
111 def _on_nbi_relation_departed(self, event: EventBase) -> NoReturn:
112 """Clears data from nbi relation.
113
114 Args:
115 event (EventBase): NBI relation event.
116 """
117 self.state.nbi_host = None
118 self.state.nbi_port = None
119 self.on.configure_pod.emit()
120
121 def _missing_relations(self) -> str:
122 """Checks if there missing relations.
123
124 Returns:
125 str: string with missing relations
126 """
127 data_status = {
128 "nbi": self.state.nbi_host,
129 }
130
131 missing_relations = [k for k, v in data_status.items() if not v]
132
133 return ", ".join(missing_relations)
134
135 @property
136 def relation_state(self) -> Dict[str, Any]:
137 """Collects relation state configuration for pod spec assembly.
138
139 Returns:
140 Dict[str, Any]: relation state information.
141 """
142 relation_state = {
143 "nbi_host": self.state.nbi_host,
144 "nbi_port": self.state.nbi_port,
145 }
146 return relation_state
147
148 def configure_pod(self, event: EventBase) -> NoReturn:
149 """Assemble the pod spec and apply it, if possible.
150
151 Args:
152 event (EventBase): Hook or Relation event that started the
153 function.
154 """
155 if missing := self._missing_relations():
156 self.unit.status = BlockedStatus(
157 f"Waiting for {missing} relation{'s' if ',' in missing else ''}"
158 )
159 return
160
161 if not self.unit.is_leader():
162 self.unit.status = ActiveStatus("ready")
163 return
164
165 self.unit.status = MaintenanceStatus("Assembling pod spec")
166
167 # Fetch image information
168 try:
169 self.unit.status = MaintenanceStatus("Fetching image information")
170 image_info = self.image.fetch()
171 except OCIImageResourceError:
172 self.unit.status = BlockedStatus("Error fetching image information")
173 return
174
175 try:
176 pod_spec = make_pod_spec(
177 image_info,
178 self.config,
179 self.relation_state,
180 self.model.app.name,
181 )
182 except ValidationError as exc:
183 logger.exception("Config/Relation data validation error")
184 self.unit.status = BlockedStatus(str(exc))
185 return
186
187 if self.state.pod_spec != pod_spec:
188 self.model.pod.set_spec(pod_spec)
189 self.state.pod_spec = pod_spec
190
191 self.unit.status = ActiveStatus("ready")
192
193
194 if __name__ == "__main__":
195 main(NgUiCharm)