Coverage for hooks/upgrade_charm.py : 0%

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
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
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
23from common import update_localhost
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")
49# this global var will collect contactgroup members that must be forced
50# it will be changed by functions
51forced_contactgroup_members = []
53HTTP_ENABLED = ssl_config not in ["only"]
54SSL_CONFIGURED = ssl_config in ["on", "only", "true"]
57def warn_legacy_relations():
58 """Checks the charm relations for legacy relations.
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")
72def parse_extra_contacts(yaml_string):
73 """Parses a list of extra Nagios contacts from a YAML string.
75 Does basic sanitization only
76 """
77 # Final result
78 extra_contacts = []
80 # Valid characters for contact names
81 valid_name_chars = string.ascii_letters + string.digits + "_-"
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")
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
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
102 if "\n" in (contact["host"] + contact["service"]):
103 hookenv.log("Line breaks not allowed in commands", hookenv.WARNING)
104 continue
106 contact["name"] = contact["name"].lower()
107 contact["alias"] = contact["name"].capitalize()
108 extra_contacts.append(contact)
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 )
121 return extra_contacts
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)
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
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])
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")
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)
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 )
185def enable_pagerduty_config():
186 global forced_contactgroup_members
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 ""
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 }
204 with open("hooks/templates/pagerduty_nagios_cfg.tmpl", "r") as f:
205 templateDef = f.read()
207 t = Template(templateDef)
208 with open(pagerduty_cfg, "w") as f:
209 f.write(t.render(template_values))
211 with open("hooks/templates/nagios-pagerduty-flush-cron.tmpl", "r") as f2:
212 templateDef = f2.read()
214 t2 = Template(templateDef)
215 with open(pagerduty_cron, "w") as f2:
216 f2.write(t2.render(template_values))
218 # Ship the pagerduty_nagios.pl script
219 shutil.copy("files/pagerduty_nagios.pl", "/usr/local/bin/pagerduty_nagios.pl")
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)
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")
243def enable_traps_config():
244 global forced_contactgroup_members
246 send_traps_to = hookenv.config("send_traps_to")
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
254 hookenv.log("Send traps feature is enabled, target address is %s" % send_traps_to)
256 if "managementstation" not in contactgroup_members:
257 forced_contactgroup_members.append("managementstation")
259 template_values = {"send_traps_to": send_traps_to}
261 with open("hooks/templates/traps.tmpl", "r") as f:
262 templateDef = f.read()
264 t = Template(templateDef)
265 with open(traps_cfg, "w") as f:
266 f.write(t.render(template_values))
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 )
292 admin_members = ", ".join([c["contact_name"] for c in contacts])
294 resulting_members = contactgroup_members
295 if admin_members:
296 # if multiple admin emails are passed ignore contactgroup_members
297 resulting_members = admin_members
299 if forced_contactgroup_members:
300 resulting_members = ",".join([resulting_members] + forced_contactgroup_members)
302 # Parse extra_contacts
303 extra_contacts = parse_extra_contacts(hookenv.config("extra_contacts"))
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 }
329 with open("hooks/templates/contacts-cfg.tmpl", "r") as f:
330 template_def = f.read()
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))
336 host.service_reload("nagios3")
339def ssl_configured():
340 allowed_options = ["on", "only"]
341 if str(ssl_config).lower() in allowed_options:
342 return True
343 return False
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)
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
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"))))
381def enable_ssl():
382 # Set the basename of all ssl files
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")
398def nagios_bool(value):
399 """Convert a Python boolean into Nagios 0/1 integer representation."""
400 return int(value)
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 }
439 with open("hooks/templates/nagios-cfg.tmpl", "r") as f:
440 templateDef = f.read()
442 t = Template(templateDef)
443 with open(nagios_cfg, "w") as f:
444 f.write(t.render(template_values))
446 with open("hooks/templates/localhost_nagios2.cfg.tmpl", "r") as f:
447 templateDef = f.read()
449 t = Template(templateDef)
450 with open("/etc/nagios3/conf.d/localhost_nagios2.cfg", "w") as f:
451 f.write(t.render(template_values))
453 host.service_reload("nagios3")
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()
461 t = Template(templateDef)
462 with open(nagios_cgi_cfg, "w") as f:
463 f.write(t.render(template_values))
465 host.service_reload("nagios3")
466 host.service_reload("apache2")
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 """
481 # Start by Setting the ports.conf
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"
488 with open(ports_conf, "w") as f:
489 f.write(t.render({"enable_http": HTTP_ENABLED}))
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()
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))
509 # Create directory for extra *.include files installed by subordinates
510 try:
511 os.makedirs("/etc/apache2/vhost.d/")
512 except OSError:
513 pass
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)
522 # Configure the behavior of https site
523 Apache2Site("default-ssl").action(enabled=SSL_CONFIGURED)
525 # Finally, restart apache2
526 host.service_reload("apache2")
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
535 def action(self, enabled):
536 return self._enable() if enabled else self._disable()
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 )
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)
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)
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])
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)
599subprocess.call(["scripts/postfix_loopback_only.sh"])
600subprocess.call(["hooks/mymonitors-relation-joined"])
601subprocess.call(["hooks/monitors-relation-changed"])