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 python 

2 

3# Rewritten from bash to python 3/2/2014 for charm helper inclusion 

4# of SSL-Everywhere! 

5import base64 

6from jinja2 import Template 

7import glob 

8import os 

9 

10# import re 

11import pwd 

12import grp 

13import string 

14import stat 

15import errno 

16import shutil 

17import subprocess 

18import yaml 

19from charmhelpers.contrib import ssl 

20from charmhelpers.core import hookenv, host 

21from charmhelpers import fetch 

22 

23from common import update_localhost 

24 

25# Gather facts 

26legacy_relations = hookenv.config("legacy") 

27extra_config = hookenv.config("extraconfig") 

28enable_livestatus = hookenv.config("enable_livestatus") 

29livestatus_path = hookenv.config("livestatus_path") 

30enable_pagerduty = hookenv.config("enable_pagerduty") 

31pagerduty_key = hookenv.config("pagerduty_key") 

32pagerduty_path = hookenv.config("pagerduty_path") 

33notification_levels = hookenv.config("pagerduty_notification_levels") 

34nagios_user = hookenv.config("nagios_user") 

35nagios_group = hookenv.config("nagios_group") 

36ssl_config = str(hookenv.config("ssl")).lower() 

37charm_dir = os.environ["CHARM_DIR"] 

38cert_domain = hookenv.unit_get("public-address") 

39nagios_cfg = "/etc/nagios3/nagios.cfg" 

40nagios_cgi_cfg = "/etc/nagios3/cgi.cfg" 

41pagerduty_cfg = "/etc/nagios3/conf.d/pagerduty_nagios.cfg" 

42traps_cfg = "/etc/nagios3/conf.d/traps.cfg" 

43pagerduty_cron = "/etc/cron.d/nagios-pagerduty-flush" 

44password = hookenv.config("password") 

45ro_password = hookenv.config("ro-password") 

46nagiosadmin = hookenv.config("nagiosadmin") or "nagiosadmin" 

47contactgroup_members = hookenv.config("contactgroup-members") 

48 

49# this global var will collect contactgroup members that must be forced 

50# it will be changed by functions 

51forced_contactgroup_members = [] 

52 

53HTTP_ENABLED = ssl_config not in ["only"] 

54SSL_CONFIGURED = ssl_config in ["on", "only", "true"] 

55 

56 

57def warn_legacy_relations(): 

58 """Checks the charm relations for legacy relations. 

59 

60 Inserts warnings into the log about legacy relations, as they will be removed 

61 in the future 

62 """ 

63 if legacy_relations is not None: 

64 hookenv.log( 

65 "Relations have been radically changed." 

66 " The monitoring interface is not supported anymore.", 

67 "WARNING", 

68 ) 

69 hookenv.log("Please use the generic juju-info or the monitors interface", "WARNING") 

70 

71 

72def parse_extra_contacts(yaml_string): 

73 """Parses a list of extra Nagios contacts from a YAML string. 

74 

75 Does basic sanitization only 

76 """ 

77 # Final result 

78 extra_contacts = [] 

79 

80 # Valid characters for contact names 

81 valid_name_chars = string.ascii_letters + string.digits + "_-" 

82 

83 try: 

84 extra_contacts_raw = yaml.load(yaml_string, Loader=yaml.SafeLoader) or [] 

85 if not isinstance(extra_contacts_raw, list): 

86 raise ValueError("not a list") 

87 

88 for contact in extra_contacts_raw: 

89 if {"name", "host", "service"} > set(contact.keys()): 

90 hookenv.log( 

91 "Contact {} is missing fields.".format(contact), hookenv.WARNING 

92 ) 

93 continue 

94 

95 if set(contact["name"]) > set(valid_name_chars): 

96 hookenv.log( 

97 "Contact name {} is illegal".format(contact["name"]), 

98 hookenv.WARNING, 

99 ) 

100 continue 

101 

102 if "\n" in (contact["host"] + contact["service"]): 

103 hookenv.log("Line breaks not allowed in commands", hookenv.WARNING) 

104 continue 

105 

106 contact["name"] = contact["name"].lower() 

107 contact["alias"] = contact["name"].capitalize() 

108 extra_contacts.append(contact) 

109 

110 except (ValueError, yaml.error.YAMLError) as e: 

111 hookenv.log( 

112 'Invalid "extra_contacts" configuration: {}'.format(e), hookenv.WARNING 

113 ) 

114 if len(extra_contacts_raw) != len(extra_contacts): 

115 hookenv.log( 

116 "Invalid extra_contacts config, found {} contacts defined, " 

117 "only {} were valid, check unit logs for " 

118 "detailed errors".format(len(extra_contacts_raw), len(extra_contacts)) 

119 ) 

120 

121 return extra_contacts 

122 

123 

124# If the charm has extra configuration provided, write that to the 

125# proper nagios3 configuration file, otherwise remove the config 

126def write_extra_config(): 

127 # Be predjudice about this - remove the file always. 

128 if host.file_hash("/etc/nagios3/conf.d/extra.cfg") is not None: 

129 os.remove("/etc/nagios3/conf.d/extra.cfg") 

130 # If we have a config, then write it. the hook reconfiguration will 

131 # handle the details 

132 if extra_config is not None: 

133 host.write_file("/etc/nagios3/conf.d/extra.cfg", extra_config) 

134 

135 

136# Equivalent of mkdir -p, since we can't rely on 

137# python 3.2 os.makedirs exist_ok argument 

138def mkdir_p(path): 

139 try: 

140 os.makedirs(path) 

141 except OSError as exc: # Python >2.5 

142 if exc.errno == errno.EEXIST and os.path.isdir(path): 

143 pass 

144 else: 

145 raise 

146 

147 

148# Fix the path to be world executable 

149def fixpath(path): 

150 if os.path.isdir(path): 

151 st = os.stat(path) 

152 os.chmod(path, st.st_mode | stat.S_IXOTH) 

153 if path != "/": 

154 fixpath(os.path.split(path)[0]) 

155 

156 

157def enable_livestatus_config(): 

158 if enable_livestatus: 

159 hookenv.log("Livestatus is enabled") 

160 fetch.apt_update() 

161 fetch.apt_install("check-mk-livestatus") 

162 

163 # Make the directory and fix perms on it 

164 hookenv.log("Fixing perms on livestatus_path") 

165 livestatus_dir = os.path.dirname(livestatus_path) 

166 if not os.path.isdir(livestatus_dir): 

167 hookenv.log("Making path for livestatus_dir") 

168 mkdir_p(livestatus_dir) 

169 fixpath(livestatus_dir) 

170 

171 # Fix the perms on the socket 

172 hookenv.log("Fixing perms on the socket") 

173 uid = pwd.getpwnam(nagios_user).pw_uid 

174 gid = grp.getgrnam("www-data").gr_gid 

175 os.chown(livestatus_path, uid, gid) 

176 os.chown(livestatus_dir, uid, gid) 

177 st = os.stat(livestatus_path) 

178 os.chmod(livestatus_path, st.st_mode | stat.S_IRGRP) 

179 os.chmod( 

180 livestatus_dir, 

181 st.st_mode | stat.S_IRGRP | stat.S_ISGID | stat.S_IXUSR | stat.S_IXGRP, 

182 ) 

183 

184 

185def enable_pagerduty_config(): 

186 global forced_contactgroup_members 

187 

188 if enable_pagerduty: 

189 hookenv.log("Pagerduty is enabled") 

190 fetch.apt_update() 

191 fetch.apt_install("libhttp-parser-perl") 

192 env = os.environ 

193 proxy = env.get("JUJU_CHARM_HTTPS_PROXY") or env.get("https_proxy") 

194 proxy_switch = "--proxy {}".format(proxy) if proxy else "" 

195 

196 # Ship the pagerduty_nagios.cfg file 

197 template_values = { 

198 "pagerduty_key": pagerduty_key, 

199 "pagerduty_path": pagerduty_path, 

200 "proxy_switch": proxy_switch, 

201 "notification_levels": notification_levels, 

202 } 

203 

204 with open("hooks/templates/pagerduty_nagios_cfg.tmpl", "r") as f: 

205 templateDef = f.read() 

206 

207 t = Template(templateDef) 

208 with open(pagerduty_cfg, "w") as f: 

209 f.write(t.render(template_values)) 

210 

211 with open("hooks/templates/nagios-pagerduty-flush-cron.tmpl", "r") as f2: 

212 templateDef = f2.read() 

213 

214 t2 = Template(templateDef) 

215 with open(pagerduty_cron, "w") as f2: 

216 f2.write(t2.render(template_values)) 

217 

218 # Ship the pagerduty_nagios.pl script 

219 shutil.copy("files/pagerduty_nagios.pl", "/usr/local/bin/pagerduty_nagios.pl") 

220 

221 # Create the pagerduty queue dir 

222 if not os.path.isdir(pagerduty_path): 

223 hookenv.log("Making path for pagerduty_path") 

224 mkdir_p(pagerduty_path) 

225 # Fix the perms on it 

226 uid = pwd.getpwnam(nagios_user).pw_uid 

227 gid = grp.getgrnam(nagios_group).gr_gid 

228 os.chown(pagerduty_path, uid, gid) 

229 else: 

230 # Clean up the files if we don't want pagerduty 

231 if os.path.isfile(pagerduty_cfg): 

232 os.remove(pagerduty_cfg) 

233 if os.path.isfile(pagerduty_cron): 

234 os.remove(pagerduty_cron) 

235 

236 # Update contacts for admin 

237 if enable_pagerduty: 

238 # avoid duplicates 

239 if "pagerduty" not in contactgroup_members: 

240 forced_contactgroup_members.append("pagerduty") 

241 

242 

243def enable_traps_config(): 

244 global forced_contactgroup_members 

245 

246 send_traps_to = hookenv.config("send_traps_to") 

247 

248 if not send_traps_to: 

249 if os.path.isfile(traps_cfg): 

250 os.remove(traps_cfg) 

251 hookenv.log("Send traps feature is disabled") 

252 return 

253 

254 hookenv.log("Send traps feature is enabled, target address is %s" % send_traps_to) 

255 

256 if "managementstation" not in contactgroup_members: 

257 forced_contactgroup_members.append("managementstation") 

258 

259 template_values = {"send_traps_to": send_traps_to} 

260 

261 with open("hooks/templates/traps.tmpl", "r") as f: 

262 templateDef = f.read() 

263 

264 t = Template(templateDef) 

265 with open(traps_cfg, "w") as f: 

266 f.write(t.render(template_values)) 

267 

268 

269def update_contacts(): 

270 # Multiple Email Contacts 

271 admin_members = "" 

272 contacts = [] 

273 admin_email = list(filter(None, set(hookenv.config("admin_email").split(",")))) 

274 if len(admin_email) == 0: 

275 hookenv.log("admin_email is unset, this isn't valid config") 

276 hookenv.status_set("blocked", "admin_email is not configured") 

277 exit(1) 

278 hookenv.status_set("active", "ready") 

279 if len(admin_email) == 1: 

280 hookenv.log("Setting one admin email address '%s'" % admin_email[0]) 

281 contacts = [{"contact_name": "root", "alias": "Root", "email": admin_email[0]}] 

282 elif len(admin_email) > 1: 

283 hookenv.log("Setting %d admin email addresses" % len(admin_email)) 

284 contacts = [] 

285 for email in admin_email: 

286 contact_name = email.replace("@", "").replace(".", "").lower() 

287 contact_alias = contact_name.capitalize() 

288 contacts.append( 

289 {"contact_name": contact_name, "alias": contact_alias, "email": email} 

290 ) 

291 

292 admin_members = ", ".join([c["contact_name"] for c in contacts]) 

293 

294 resulting_members = contactgroup_members 

295 if admin_members: 

296 # if multiple admin emails are passed ignore contactgroup_members 

297 resulting_members = admin_members 

298 

299 if forced_contactgroup_members: 

300 resulting_members = ",".join([resulting_members] + forced_contactgroup_members) 

301 

302 # Parse extra_contacts 

303 extra_contacts = parse_extra_contacts(hookenv.config("extra_contacts")) 

304 

305 template_values = { 

306 "admin_service_notification_period": hookenv.config( 

307 "admin_service_notification_period" 

308 ), 

309 "admin_host_notification_period": hookenv.config( 

310 "admin_host_notification_period" 

311 ), 

312 "admin_service_notification_options": hookenv.config( 

313 "admin_service_notification_options" 

314 ), 

315 "admin_host_notification_options": hookenv.config( 

316 "admin_host_notification_options" 

317 ), 

318 "admin_service_notification_commands": hookenv.config( 

319 "admin_service_notification_commands" 

320 ), 

321 "admin_host_notification_commands": hookenv.config( 

322 "admin_host_notification_commands" 

323 ), 

324 "contacts": contacts, 

325 "contactgroup_members": resulting_members, 

326 "extra_contacts": extra_contacts, 

327 } 

328 

329 with open("hooks/templates/contacts-cfg.tmpl", "r") as f: 

330 template_def = f.read() 

331 

332 t = Template(template_def) 

333 with open("/etc/nagios3/conf.d/contacts_nagios2.cfg", "w") as f: 

334 f.write(t.render(template_values)) 

335 

336 host.service_reload("nagios3") 

337 

338 

339def ssl_configured(): 

340 allowed_options = ["on", "only"] 

341 if str(ssl_config).lower() in allowed_options: 

342 return True 

343 return False 

344 

345 

346# Gather local facts for SSL deployment 

347deploy_key_path = os.path.join(charm_dir, "data", "%s.key" % (cert_domain)) 

348deploy_cert_path = os.path.join(charm_dir, "data", "%s.crt" % (cert_domain)) 

349deploy_csr_path = os.path.join(charm_dir, "data", "%s.csr" % (cert_domain)) 

350# set basename for SSL key locations 

351cert_file = "/etc/ssl/certs/%s.pem" % (cert_domain) 

352key_file = "/etc/ssl/private/%s.key" % (cert_domain) 

353chain_file = "/etc/ssl/certs/%s.csr" % (cert_domain) 

354 

355 

356# Check for key and certificate, since the CSR is optional 

357# leave it out of the dir file check and let the config manager 

358# worry about it 

359def check_ssl_files(): 

360 key = os.path.exists(deploy_key_path) 

361 cert = os.path.exists(deploy_cert_path) 

362 if key is False or cert is False: 

363 return False 

364 return True 

365 

366 

367# Decode the SSL keys from their base64 encoded values in the configuration 

368def decode_ssl_keys(): 

369 if hookenv.config("ssl_key"): 

370 hookenv.log("Writing key from config ssl_key: %s" % key_file) 

371 with open(key_file, "w") as f: 

372 f.write(str(base64.b64decode(hookenv.config("ssl_key")))) 

373 if hookenv.config("ssl_cert"): 

374 with open(cert_file, "w") as f: 

375 f.write(str(base64.b64decode(hookenv.config("ssl_cert")))) 

376 if hookenv.config("ssl_chain"): 

377 with open(chain_file, "w") as f: 

378 f.write(str(base64.b64decode(hookenv.config("ssl_cert")))) 

379 

380 

381def enable_ssl(): 

382 # Set the basename of all ssl files 

383 

384 # Validate that we have configs, and generate a self signed certificate. 

385 if not hookenv.config("ssl_cert"): 

386 # bail if keys already exist 

387 if os.path.exists(cert_file): 

388 hookenv.log("Keys exist, not creating keys!", "WARNING") 

389 return 

390 # Generate a self signed key using CharmHelpers 

391 hookenv.log("Generating Self Signed Certificate", "INFO") 

392 ssl.generate_selfsigned(key_file, cert_file, cn=cert_domain) 

393 else: 

394 decode_ssl_keys() 

395 hookenv.log("Decoded SSL files", "INFO") 

396 

397 

398def nagios_bool(value): 

399 """Convert a Python boolean into Nagios 0/1 integer representation.""" 

400 return int(value) 

401 

402 

403def update_config(): 

404 host_context = hookenv.config("nagios_host_context") 

405 local_host_name = "nagios" 

406 principal_unitname = hookenv.principal_unit() 

407 # Fallback to using "primary" if it exists. 

408 if principal_unitname: 

409 local_host_name = principal_unitname 

410 else: 

411 local_host_name = hookenv.local_unit().replace("/", "-") 

412 template_values = { 

413 "nagios_user": nagios_user, 

414 "nagios_group": nagios_group, 

415 "enable_livestatus": enable_livestatus, 

416 "livestatus_path": livestatus_path, 

417 "livestatus_args": hookenv.config("livestatus_args"), 

418 "check_external_commands": hookenv.config("check_external_commands"), 

419 "command_check_interval": hookenv.config("command_check_interval"), 

420 "command_file": hookenv.config("command_file"), 

421 "debug_file": hookenv.config("debug_file"), 

422 "debug_verbosity": hookenv.config("debug_verbosity"), 

423 "debug_level": hookenv.config("debug_level"), 

424 "daemon_dumps_core": hookenv.config("daemon_dumps_core"), 

425 "flap_detection": nagios_bool(hookenv.config("flap_detection")), 

426 "admin_email": hookenv.config("admin_email"), 

427 "admin_pager": hookenv.config("admin_pager"), 

428 "log_rotation_method": hookenv.config("log_rotation_method"), 

429 "log_archive_path": hookenv.config("log_archive_path"), 

430 "use_syslog": hookenv.config("use_syslog"), 

431 "monitor_self": hookenv.config("monitor_self"), 

432 "nagios_hostname": "{}-{}".format(host_context, local_host_name), 

433 "load_monitor": hookenv.config("load_monitor"), 

434 "is_container": host.is_container(), 

435 "service_check_timeout": hookenv.config("service_check_timeout"), 

436 "service_check_timeout_state": hookenv.config("service_check_timeout_state"), 

437 } 

438 

439 with open("hooks/templates/nagios-cfg.tmpl", "r") as f: 

440 templateDef = f.read() 

441 

442 t = Template(templateDef) 

443 with open(nagios_cfg, "w") as f: 

444 f.write(t.render(template_values)) 

445 

446 with open("hooks/templates/localhost_nagios2.cfg.tmpl", "r") as f: 

447 templateDef = f.read() 

448 

449 t = Template(templateDef) 

450 with open("/etc/nagios3/conf.d/localhost_nagios2.cfg", "w") as f: 

451 f.write(t.render(template_values)) 

452 

453 host.service_reload("nagios3") 

454 

455 

456def update_cgi_config(): 

457 template_values = {"nagiosadmin": nagiosadmin, "ro_password": ro_password} 

458 with open("hooks/templates/nagios-cgi.tmpl", "r") as f: 

459 templateDef = f.read() 

460 

461 t = Template(templateDef) 

462 with open(nagios_cgi_cfg, "w") as f: 

463 f.write(t.render(template_values)) 

464 

465 host.service_reload("nagios3") 

466 host.service_reload("apache2") 

467 

468 

469# Nagios3 is deployed as a global apache application from the archive. 

470# We'll get a little funky and add the SSL keys to the default-ssl config 

471# which sets our keys, including the self-signed ones, as the host keyfiles. 

472# note: i tried to use cheetah, and it barfed, several times. It can go play 

473# in a fire. I'm jusing jinja2. 

474def update_apache(): 

475 """ 

476 Nagios3 is deployed as a global apache application from the archive. 

477 We'll get a little funky and add the SSL keys to the default-ssl config 

478 which sets our keys, including the self-signed ones, as the host keyfiles. 

479 """ 

480 

481 # Start by Setting the ports.conf 

482 

483 with open("hooks/templates/ports-cfg.jinja2", "r") as f: 

484 template_def = f.read() 

485 t = Template(template_def) 

486 ports_conf = "/etc/apache2/ports.conf" 

487 

488 with open(ports_conf, "w") as f: 

489 f.write(t.render({"enable_http": HTTP_ENABLED})) 

490 

491 # Next setup the default-ssl.conf 

492 if os.path.exists(chain_file) and os.path.getsize(chain_file) > 0: 

493 ssl_chain = chain_file 

494 else: 

495 ssl_chain = None 

496 template_values = { 

497 "ssl_key": key_file, 

498 "ssl_cert": cert_file, 

499 "ssl_chain": ssl_chain, 

500 } 

501 with open("hooks/templates/default-ssl.tmpl", "r") as f: 

502 template_def = f.read() 

503 

504 t = Template(template_def) 

505 ssl_conf = "/etc/apache2/sites-available/default-ssl.conf" 

506 with open(ssl_conf, "w") as f: 

507 f.write(t.render(template_values)) 

508 

509 # Create directory for extra *.include files installed by subordinates 

510 try: 

511 os.makedirs("/etc/apache2/vhost.d/") 

512 except OSError: 

513 pass 

514 

515 # Configure the behavior of http sites 

516 sites = glob.glob("/etc/apache2/sites-available/*.conf") 

517 non_ssl = set(sites) - {ssl_conf} 

518 for each in non_ssl: 

519 site = os.path.basename(each).rsplit(".", 1)[0] 

520 Apache2Site(site).action(enabled=HTTP_ENABLED) 

521 

522 # Configure the behavior of https site 

523 Apache2Site("default-ssl").action(enabled=SSL_CONFIGURED) 

524 

525 # Finally, restart apache2 

526 host.service_reload("apache2") 

527 

528 

529class Apache2Site: 

530 def __init__(self, site): 

531 self.site = site 

532 self.is_ssl = "ssl" in site.lower() 

533 self.port = 443 if self.is_ssl else 80 

534 

535 def action(self, enabled): 

536 return self._enable() if enabled else self._disable() 

537 

538 def _call(self, args): 

539 try: 

540 subprocess.check_output(args, stderr=subprocess.STDOUT) 

541 except subprocess.CalledProcessError as e: 

542 hookenv.log( 

543 "Apache2Site: `{}`, returned {}, stdout:\n{}".format( 

544 e.cmd, e.returncode, e.output 

545 ), 

546 "ERROR", 

547 ) 

548 

549 def _enable(self): 

550 hookenv.log("Apache2Site: Enabling %s..." % self.site, "INFO") 

551 self._call(["a2ensite", self.site]) 

552 if self.port == 443: 

553 self._call(["a2enmod", "ssl"]) 

554 hookenv.open_port(self.port) 

555 

556 def _disable(self): 

557 hookenv.log("Apache2Site: Disabling %s..." % self.site, "INFO") 

558 self._call(["a2dissite", self.site]) 

559 hookenv.close_port(self.port) 

560 

561 

562def update_password(account, password): 

563 """Update the charm and Apache's record of the password for the supplied account.""" 

564 account_file = "".join(["/var/lib/juju/nagios.", account, ".passwd"]) 

565 if password: 

566 with open(account_file, "w") as f: 

567 f.write(password) 

568 os.fchmod(f.fileno(), 0o0400) 

569 subprocess.call( 

570 ["htpasswd", "-b", "/etc/nagios3/htpasswd.users", account, password] 

571 ) 

572 else: 

573 """ password was empty, it has been removed. We should delete the account """ 

574 os.path.isfile(account_file) and os.remove(account_file) 

575 subprocess.call(["htpasswd", "-D", "/etc/nagios3/htpasswd.users", account]) 

576 

577 

578warn_legacy_relations() 

579write_extra_config() 

580# enable_traps_config and enable_pagerduty_config modify forced_contactgroup_members 

581# they need to run before update_contacts that will consume that global var. 

582enable_traps_config() 

583if ssl_configured(): 

584 enable_ssl() 

585enable_pagerduty_config() 

586update_contacts() 

587update_config() 

588enable_livestatus_config() 

589update_apache() 

590update_localhost() 

591update_cgi_config() 

592update_contacts() 

593update_password("nagiosro", ro_password) 

594if password: 

595 update_password(nagiosadmin, password) 

596if nagiosadmin != "nagiosadmin": 

597 update_password("nagiosadmin", False) 

598 

599subprocess.call(["scripts/postfix_loopback_only.sh"]) 

600subprocess.call(["hooks/mymonitors-relation-joined"]) 

601subprocess.call(["hooks/monitors-relation-changed"])