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"""Implements prometheus_openstack_exporter reactive state machine. 

2 

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

17 

18import base64 

19import functools 

20import os 

21 

22from charmhelpers.contrib.charmsupport import nrpe 

23from charmhelpers.core import hookenv, host, unitdata 

24from charmhelpers.core.templating import render 

25 

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 

35 

36from jinja2 import Template 

37 

38import yaml 

39 

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" 

44 

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} 

54 

55 

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 

60 

61 

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

71 

72 

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

84 

85 

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) 

95 

96 

97@when("identity-credentials.connected") 

98def configure_keystone_username(keystone): 

99 username = "prometheus-openstack-exporter" 

100 keystone.configure(username) 

101 

102 

103@when("identity-credentials.available") 

104def save_creds(keystone): 

105 reconfig_on_change("keystone-relation-creds", keystone.get_creds()) 

106 

107 

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

114 

115 

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 

125 

126 

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 

179 

180 

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

233 

234 

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) 

242 

243 

244@when("exporter.do-restart") 

245def do_restart(): 

246 restart_service() 

247 hookenv.status_set("active", "Ready") 

248 remove_state("exporter.do-restart") 

249 

250 

251@when("prometheus-openstack-exporter-service.available") 

252def configure_exporter_service(exporter_service): 

253 config = hookenv.config() 

254 exporter_service.configure(config.get("port")) 

255 

256 

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

276 

277 

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

296 

297 

298def render_template(template, context): 

299 tmpl = Template(template, lstrip_blocks=True, trim_blocks=True) 

300 return tmpl.render(**context)