← Back to team overview

nagios-charmers team mailing list archive

Re: [Merge] ~peppepetra86/charm-nagios:extra_contacts into charm-nagios:master

 

Review: Needs Fixing

Hi Giuseppe:

Added some inline comments, most of them are non-blocker.

Diff comments:

> diff --git a/hooks/upgrade_charm.py b/hooks/upgrade_charm.py
> index bbbb8ac..cc20507 100755
> --- a/hooks/upgrade_charm.py
> +++ b/hooks/upgrade_charm.py
> @@ -58,6 +69,53 @@ def warn_legacy_relations():
>                  "WARNING")
>  
>  
> +# Parses a list of extra Nagios contacts from a YAML string
> +# Does basic sanitization only
> +def get_extra_contacts(yaml_string, log=False):

personal opinion: the log param seems pointless here.

> +    # Final result
> +    extra_contacts = []
> +
> +    # Valid characters for contact names
> +    valid_name_chars = (
> +        'abcdefghijklmnopqrstuvwxyz'
> +        'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
> +        '0123456789_-'
> +    )

non-blocker: this could be:  string.ascii_letters + string.digits + '_-'

> +
> +    try:
> +        extra_contacts_raw = yaml.load(yaml_string, Loader=yaml.SafeLoader) or []
> +        if not isinstance(extra_contacts_raw, list):
> +            raise ValueError('not a list')
> +
> +        for contact in extra_contacts_raw:
> +            if {'name', 'host', 'service'} > set(contact.keys()):
> +                if log:
> +                    hookenv.log(
> +                        'Contact {} is missing fields.'.format(contact))

should be `contact.name`, although contact will also work, as a dict.
log level should be a WARNING or ERROR.

> +                continue
> +
> +            if set(contact['name']) >= set(valid_name_chars):

`>=` --> `>`
equal should be fine here

> +                if log:
> +                    hookenv.log(
> +                        'Contact name {} is illegal'.format(contact['name']))

log level should be a WARNING or ERROR.

> +                continue
> +
> +            if '\n' in (contact['host'] + contact['service']):
> +                if log:
> +                    hookenv.log('Line breaks not allowed in commands')

log level should be a WARNING or ERROR.

> +                continue
> +            contact['name'] = contact['name'].lower()
> +            contact['alias'] = contact['name'].capitalize()
> +            extra_contacts.append(contact)
> +
> +    except (ValueError, yaml.error.YAMLError) as e:
> +        if log:
> +            hookenv.log(
> +                'Invalid "extra_contacts" configuration: {}'.format(e))

log level should be a WARNING or ERROR.

> +
> +    return extra_contacts
> +
> +
>  # If the charm has extra configuration provided, write that to the
>  # proper nagios3 configuration file, otherwise remove the config
>  def write_extra_config():
> @@ -204,6 +262,40 @@ def update_contacts():
>  
>      if forced_contactgroup_members:
>          resulting_members = resulting_members + ',' + ','.join(forced_contactgroup_members)
> +    # Multiple Email Contacts
> +    contacts = []
> +    admin_email = list(
> +        filter(None, set(hookenv.config('admin_email').split(',')))
> +    )
> +    if len(admin_email) == 0:
> +        hookenv.log("admin_email is unset, this isn't valid config")
> +        hookenv.status_set("blocked", "admin_email is not configured")
> +        exit(0)

personal opinion: maybe use `return` instead of `exit` here?
I know that will introduce some other adjustment, but exit in the middle of a util function seems not a good practice to me.

> +    hookenv.status_set("active", "ready")
> +    if len(admin_email) == 1:
> +        hookenv.log("Setting one admin email address '%s'" % admin_email[0])
> +        contacts = [{
> +            'contact_name': 'root',
> +            'alias': 'Root',
> +            'email': admin_email[0]
> +        }]
> +    elif len(admin_email) > 1:
> +        hookenv.log("Setting %d admin email addresses" % len(admin_email))
> +        contacts = [
> +            {
> +                'contact_name': email.lower(),
> +                'alias': email.capitalize(),
> +                'email': email
> +            }
> +            for email in admin_email
> +        ]
> +        resulting_members = ', '.join([
> +            c['contact_name'] for c in contacts
> +        ])

This whole admin_email section looks weird to me:
1) that filter+None+set+line is too fancy
2) why we have to call the only admin `root`?
3) we only allow [a-zA-Z_-] for extra_contact name, but here we use email, which has invalid chars like @ or dot. Not a issue but inconsistent.

Also, for current code, I will prefer to write it like this:

admin_email = hookenv.config('admin_email')
if not admin_email:
    ...
    return # or exit

if ',' in admin_email:
    admin_email = {email for email in admin_email.split(',') if email}
    ...
else:
    ...

> +
> +    # Parse extra_contacts
> +    extra_contacts = get_extra_contacts(
> +        hookenv.config('extra_contacts'), log=True)
>  
>      template_values = {'admin_service_notification_period': hookenv.config('admin_service_notification_period'),
>                         'admin_host_notification_period': hookenv.config('admin_host_notification_period'),
> @@ -371,27 +481,60 @@ def update_apache():
>          templateDef = f.read()
>  
>      t = Template(templateDef)
> -    with open('/etc/apache2/sites-available/default-ssl.conf', 'w') as f:
> +    ssl_conf = '/etc/apache2/sites-available/default-ssl.conf'
> +    with open(ssl_conf, 'w') as f:
>          f.write(t.render(template_values))
> -    print("Value of ssl is %s" % ssl)
> -    if ssl_config == "only":
> -        subprocess.call(['a2dissite', 'default'])
> -        hookenv.close_port(80)
> -        subprocess.call(['a2ensite', 'default-ssl'])
> -        subprocess.call(['a2enmod', 'ssl'])
> -    elif ssl_config == "on":
> -        subprocess.call(['a2ensite', 'default-ssl'])
> -        subprocess.call(['a2enmod', 'ssl'])
> -        hookenv.open_port(443)
> -    else:
> -        subprocess.call(['a2dissite', 'default-ssl'])
> -        hookenv.close_port(443)
> -        subprocess.call(['a2ensite', 'default'])
> -        hookenv.open_port(80)
>  
> +    # Create directory for extra *.include files installed by subordinates
> +    try:
> +        os.makedirs('/etc/apache2/vhost.d/')

For python>=3.2: os.makedirs(path, exist_ok=True)

> +    except OSError:
> +        pass
> +
> +    # Configure the behavior of http sites
> +    sites = glob.glob("/etc/apache2/sites-available/*.conf")
> +    non_ssl = set(sites) - {ssl_conf}
> +    for each in non_ssl:
> +        site = os.path.basename(each).strip('.conf')

'config.conf'.strip('.conf') --> 'ig'
NOTE: rstrip is not right either: 'my_conf.conf'.rstrip('.conf') --> 'my_'
correct way in my opinion:
site = os.path.basename(each).rsplit(sep='.', maxsplit=1)[0]

> +        Apache2Site(site).action(enabled=HTTP_ENABLED)
> +
> +    # Configure the behavior of https site
> +    Apache2Site("default-ssl").action(enabled=SSL_CONFIGURED)
> +
> +    # Finally, restart apache2
>      host.service_reload('apache2')
>  
>  
> +class Apache2Site:
> +    def __init__(self, site):
> +        self.site = site
> +        self.is_ssl = 'ssl' in site.lower()
> +        self.port = 443 if self.is_ssl else 80
> +
> +    def action(self, enabled):
> +        fn = self._enable if enabled else self._disable
> +        return fn()

return self._enable() if enabled else self._disabled()

> +
> +    def _call(self, args):
> +        try:
> +            subprocess.check_output(args, stderr=subprocess.STDOUT)
> +        except subprocess.CalledProcessError as e:
> +            hookenv.log("Apache2Site: `{}`, returned {}, stdout:\n{}"
> +                        .format(e.cmd, e.returncode, e.output), "ERROR")
> +
> +    def _enable(self):
> +        hookenv.log("Apache2Site: Enabling %s..." % self.site, "INFO")
> +        self._call(['a2ensite', self.site])
> +        if self.port == 443:
> +            self._call(['a2enmod', 'ssl'])
> +        hookenv.open_port(self.port)
> +
> +    def _disable(self):
> +        hookenv.log("Apache2Site: Disabling %s..." % self.site, "INFO")
> +        self._call(['a2dissite', self.site])
> +        hookenv.close_port(self.port)
> +
> +
>  def update_password(account, password):
>      """Update the charm and Apache's record of the password for the supplied account."""
>      account_file = ''.join(['/var/lib/juju/nagios.', account, '.passwd'])
> diff --git a/tests/functional/test_config.py b/tests/functional/test_config.py
> index 2e1cab6..ede914d 100644
> --- a/tests/functional/test_config.py
> +++ b/tests/functional/test_config.py
> @@ -112,3 +124,21 @@ async def test_pager_duty(unit, enable_pagerduty, file_stat):
>      )
>      stat = await file_stat('/etc/nagios3/conf.d/pagerduty_nagios.cfg', unit.u)
>      assert stat['size'] != 0, "pagerduty_config wasn't a non-zero sized file"
> +
> +
> +async def test_extra_contacts(auth, unit, set_extra_contacts):
> +    contancts_url = "http://%s/cgi-bin/nagios3/config.cgi?"; \
> +                    "type=contacts" % unit.u.public_address
> +    contact_name = set_extra_contacts
> +    r = requests.get(contancts_url, auth=auth)
> +    assert r.status_code == 200, "Get Nagios config request failed"
> +    assert r.text.find(contact_name), "Nagios is not loading the extra contact."
> +    assert r.text.find(contact_name.capitalize()), "Contact name alias is not " \

`str.find` will return substr index, which could be 0. Use `in` for safe.

> +                                                   "the capitalized name."
> +    contactgroups_url = "http://%s/cgi-bin/nagios3/config.cgi"; \
> +                        "?type=contactgroups" % unit.u.public_address
> +
> +    r = requests.get(contactgroups_url, auth=auth)
> +    assert r.status_code == 200, "Get Nagios config request failed"
> +    assert r.text.find(contact_name), "Extra contact is not " \

`str.find` will return substr index, which could be 0. Use `in` for safe.

> +                                      "added to the contact groups."
> diff --git a/tests/functional/test_deploy.py b/tests/functional/test_deploy.py
> index 9a9e067..867a527 100644
> --- a/tests/functional/test_deploy.py
> +++ b/tests/functional/test_deploy.py
> @@ -25,13 +25,12 @@ async def test_hosts_being_monitored(auth, unit):
>      host_url = ("http://%s/cgi-bin/nagios3/status.cgi?";
>                  "hostgroup=all&style=hostdetail") % unit.u.public_address
>      r = requests.get(host_url, auth=auth)
> -    assert r.text.find('mysql') and r.text.find('mediawiki'), \
> -        "Nagios is not monitoring the hosts it supposed to."
> +    assert r.text.find('mysql'), "Nagios is not monitoring the hosts it supposed to."

`str.find` will return substr index, which could be 0. Use `in` for safe.

>  
>  
>  async def test_nrpe_monitors_config(relatives, unit, file_contents):
>      # look for disk root check in nrpe config
> -    mysql_unit = relatives['mysql'].units[0]
> +    mysql_unit = relatives['mysql']['app'].units[0]
>      contents = await file_contents(
>              '/etc/nagios/nrpe.d/check_disk_root.cfg',
>              mysql_unit


-- 
https://code.launchpad.net/~peppepetra86/charm-nagios/+git/charm-nagios/+merge/388119
Your team Nagios Charm developers is subscribed to branch charm-nagios:master.


References