Coverage for reactive/openstack_exporter.py : 67%

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"""Implements prometheus_openstack_exporter reactive state machine.
3# Copyright 2016 Canonical Ltd
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16"""
18import base64
19import functools
20import os
22from charmhelpers.contrib.charmsupport import nrpe
23from charmhelpers.core import hookenv, host, unitdata
24from charmhelpers.core.templating import render
26from charms.layer import snap
27from charms.reactive import (
28 remove_state,
29 set_state,
30 when,
31 when_any,
32 when_not,
33)
34from charms.reactive.helpers import data_changed
36from jinja2 import Template
38import yaml
40SNAP_NAME = "prometheus-openstack-exporter"
41SVC_NAME = "snap.prometheus-openstack-exporter." "prometheus-openstack-exporter.service"
42VAR_SNAP_ETC = "/var/snap/prometheus-openstack-exporter/current"
43VAR_SNAP_COMMON = "/var/snap/prometheus-openstack-exporter/common"
45MAIN, CREDS, CACERT = ("main", "creds", "cacert")
46CONFIG_MAP = {
47 "main": {
48 "source": "prometheus-openstack-exporter.yaml",
49 "target": "{etcdir}/prometheus-openstack-exporter.yaml",
50 },
51 "creds": {"source": "admin.novarc", "target": "{etcdir}/admin.novarc"},
52 "cacert": {"source": "ca.crt", "target": "{etcdir}/ca.crt"},
53}
56def config_paths(key):
57 config_path = CONFIG_MAP[key]
58 config_path["target"] = config_path["target"].format(etcdir=VAR_SNAP_ETC)
59 return config_path
62def reconfig_on_change(key, data):
63 if not data_changed(key, data):
64 hookenv.log(
65 "{} data unchanged, skipping reconfig".format(key), level=hookenv.DEBUG
66 )
67 return
68 unitdata.kv().set(key, data)
69 hookenv.log("{} data changed, triggering reconfig".format(key), level=hookenv.DEBUG)
70 set_state("exporter.do-reconfig")
73@when_not("exporter.installed")
74def install_packages():
75 hookenv.status_set("maintenance", "Installing software")
76 config = hookenv.config()
77 channel = config.get("snap_channel", "stable")
78 # required for offline installs
79 # uses charm store if no "core" resource is provided
80 snap.install("core")
81 snap.install(SNAP_NAME, channel=channel, force_dangerous=False)
82 hookenv.status_set("maintenance", "Software installed")
83 set_state("exporter.installed")
86@when("swift-storage.available")
87def save_swift_hosts(swift):
88 services = swift.services()
89 swift_hosts = [
90 h["hostname"]
91 for h in functools.reduce(lambda x, y: x + y, [s["hosts"] for s in services])
92 ]
93 hookenv.log("services => {}, hosts => {}".format(services, swift_hosts))
94 reconfig_on_change("swift_hosts", swift_hosts)
97@when("identity-credentials.connected")
98def configure_keystone_username(keystone):
99 username = "prometheus-openstack-exporter"
100 keystone.configure(username)
103@when("identity-credentials.available")
104def save_creds(keystone):
105 reconfig_on_change("keystone-relation-creds", keystone.get_creds())
108@when_any("exporter.installed", "exporter.do-check-reconfig")
109def check_reconfig_exporter():
110 config = hookenv.config()
111 if data_changed("exporter.config", config):
112 set_state("exporter.do-reconfig")
113 remove_state("exporter.do-check-reconfig")
116def convert_from_base64(v):
117 if not v:
118 return v
119 if v.startswith("-----BEGIN"):
120 return v
121 try:
122 return base64.b64decode(v).decode()
123 except TypeError:
124 return v
127# allow user to override credentials (and the need to be related to keystone)
128# with 'os-credentials' YAML dict
129def get_credentials():
130 config = hookenv.config()
131 config_creds_yaml = config.get("os-credentials")
132 creds = {}
133 if config_creds_yaml:
134 config_creds = yaml.safe_load(config_creds_yaml)
135 creds = {
136 "credentials_username": config_creds["username"],
137 "credentials_password": config_creds["password"],
138 "credentials_project": config_creds.get("tenant_name", "admin"),
139 "region": config_creds["region_name"],
140 "auth_url": config_creds["auth_url"],
141 }
142 identity_api_version = config_creds.get("identity_api_version", 2)
143 if identity_api_version == 3:
144 creds["credentials_user_domain_name"] = config_creds["user_domain_name"]
145 creds["credentials_project_name"] = config_creds.get(
146 "project_name", "admin"
147 )
148 creds["credentials_identity_api_version"] = identity_api_version
149 creds["credentials_project_domain_name"] = config_creds.get(
150 "project_domain_name", "admin_domain"
151 )
152 creds["credentials_project_id"] = config_creds["project_id"]
153 creds["credentials_project_domain_id"] = config_creds["project_domain_id"]
154 else:
155 kv = unitdata.kv()
156 creds = kv.get("keystone-relation-creds")
157 # lp#1785864 - Workaround Keystone relation not yet ready.
158 if creds is None:
159 hookenv.log(
160 "get_credentials: config os-credentials not set and "
161 "identity-credentials relation not yet ready"
162 )
163 return None
164 # The "dict" given by the relation and the variables expected by the
165 # template aren't the same, so we need to manually map some values.
166 if "domain" in creds:
167 if "credentials_user_domain_name" not in creds:
168 creds["credentials_user_domain_name"] = creds["domain"]
169 if "credentials_project_domain_name" not in creds:
170 creds["credentials_project_domain_name"] = creds["domain"]
171 if "api_version" in creds:
172 if "identity_api_version" not in creds:
173 creds["credentials_identity_api_version"] = int(creds["api_version"])
174 ssl_ca = convert_from_base64(config.get("ssl_ca"))
175 if ssl_ca:
176 creds["ssl_ca"] = ssl_ca
177 creds["cacert"] = config_paths(CACERT)["target"]
178 return creds
181@when("exporter.do-reconfig")
182def render_config():
183 """Render the configuration for charm when all the interfaces are available."""
184 hookenv.status_set("maintenance", "Updating configs")
185 kv = unitdata.kv()
186 creds = get_credentials()
187 if not creds:
188 hookenv.log("render_config: No credentials yet, skipping")
189 hookenv.status_set(
190 "blocked",
191 "Waiting for credentials. Please "
192 "set os-credentials or add keystone relation",
193 )
194 return
195 hookenv.log(
196 "render_config: Got credentials for username={}".format(
197 creds["credentials_username"]
198 )
199 )
200 config = hookenv.config()
201 ctx = config
202 ctx["region"] = creds["region"]
203 ctx["cache_file"] = os.path.join(VAR_SNAP_COMMON, creds["region"])
204 ctx["cache_refresh_interval"] = config["cache-refresh-interval"]
205 ctx["cpu_allocation_ratio"] = config["cpu-allocation-ratio"]
206 ctx["ram_allocation_ratio"] = config["ram-allocation-ratio"]
207 ctx["disk_allocation_ratio"] = config["disk-allocation-ratio"]
208 ctx["schedulable_instance_size"] = config["schedulable-instance-size"].split(",")
209 ctx["swift_hosts"] = kv.get("swift_hosts")
210 ctx["use_nova_volumes"] = config["use_nova_volumes"]
211 ctx["log_level"] = config["log_level"]
212 if creds.get("ssl_ca"):
213 hookenv.log("render_config: Using provided ssl_ca")
214 # Use instead **kwargs due to PEP448 and trusty python 3.4.3
215 render(
216 source=config_paths(CACERT)["source"],
217 target=config_paths(CACERT)["target"],
218 context=creds,
219 )
220 # Use instead of **kwargs due to PEP448 and trusty python 3.4.3
221 render(
222 source=config_paths(CREDS)["source"],
223 target=config_paths(CREDS)["target"],
224 context=creds,
225 )
226 render(
227 source=config_paths(MAIN)["source"],
228 target=config_paths(MAIN)["target"],
229 context=ctx,
230 )
231 remove_state("exporter.do-reconfig")
232 set_state("exporter.do-restart")
235def restart_service():
236 if not host.service_running(SVC_NAME):
237 hookenv.log("Starting {}...".format(SVC_NAME))
238 host.service_start(SVC_NAME)
239 else:
240 hookenv.log("Restarting {}, config file changed...".format(SVC_NAME))
241 host.service_restart(SVC_NAME)
244@when("exporter.do-restart")
245def do_restart():
246 restart_service()
247 hookenv.status_set("active", "Ready")
248 remove_state("exporter.do-restart")
251@when("prometheus-openstack-exporter-service.available")
252def configure_exporter_service(exporter_service):
253 config = hookenv.config()
254 exporter_service.configure(config.get("port"))
257@when("nrpe-external-master.available")
258def update_nrpe_config(svc):
259 hostname = nrpe.get_nagios_hostname()
260 nrpe_setup = nrpe.NRPE(hostname=hostname)
261 config = hookenv.config()
262 port = config.get("port")
263 extra_nrpe_args = config.get("extra-nrpe-args")
264 # Note(aluria): check_http addresses LP#1829470 (/metrics takes too long,
265 # while / check takes ms. A final fix will land once LP#1829496 is fixed
266 # (we can't look now for "-s 'OpenStack Exporter'", which is more explicit
267 # than "-s Exporter" body content)
268 nrpe_setup.add_check(
269 "prometheus_openstack_exporter_http",
270 "Prometheus Openstack Exporter HTTP check",
271 "check_http -I 127.0.0.1 -p {} -u / -s Exporter {}".format(
272 port, extra_nrpe_args
273 ),
274 )
275 nrpe_setup.write()
278@when("prometheus-rules.available")
279def render_prometheus_rules(prometheus_rules):
280 # Send a list of rules for alerting to Prometheus
281 config = hookenv.config()
282 context = {
283 "wait_time": config.get("wait_time"),
284 "min_schedulable_instances": config.get("min_schedulable_instances"),
285 }
286 formatted_rules = []
287 # This is a list, so we can add more templates in the future with minimal changes
288 templates_dir = os.path.join(hookenv.charm_dir(), "templates")
289 template_files = [
290 "rule_schedulable_instances.j2",
291 ]
292 for template_file in template_files:
293 with open(os.path.join(templates_dir, template_file), "r") as fd:
294 formatted_rules.append(render_template(fd.read(), context))
295 prometheus_rules.configure("\n".join(formatted_rules))
298def render_template(template, context):
299 tmpl = Template(template, lstrip_blocks=True, trim_blocks=True)
300 return tmpl.render(**context)