← Back to team overview

bootstack-charmers team mailing list archive

[Merge] ~guoqiao/charm-juju-lint:dev into charm-juju-lint:master

 

Joe Guo has proposed merging ~guoqiao/charm-juju-lint:dev into charm-juju-lint:master.

Commit message:
This patch aims to:
1. make the charm work as minimal viable product.
2. complete nagios nrpe check

Requested reviews:
  Zachary Zehring (zzehring)

For more details, see:
https://code.launchpad.net/~guoqiao/charm-juju-lint/+git/charm-juju-lint/+merge/377959
-- 
Your team Canonical BootStack Charmers is subscribed to branch charm-juju-lint:master.
diff --git a/.gitignore b/.gitignore
index 6912fef..c2d0d6b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
 deps/
+__pycache__/
+.tox/
diff --git a/config.yaml b/config.yaml
index ba623bf..286fbfe 100644
--- a/config.yaml
+++ b/config.yaml
@@ -60,3 +60,22 @@ options:
     default: "ppa:juju-linters/ppa"
   install_keys:
     default: "null"
+
+  nagios_context:
+    type: string
+    default: "juju"
+    description: |
+      Used by the nrpe-external-master subordinate charm.
+      A string that will be prepended to instance name to set the host name
+      in nagios. So for instance the hostname would be something like:
+      .
+          juju-myservice-0
+      .
+      If you're running multiple environments with the same services in them
+      this allows you to differentiate between them.
+  nagios_servicegroups:
+    type: string
+    default: ""
+    description: |
+      A comma-separated list of nagios servicegroups.
+      If left empty, the nagios_context will be used as the servicegroup.
diff --git a/layer.yaml b/layer.yaml
index 89f92b1..9021a46 100644
--- a/layer.yaml
+++ b/layer.yaml
@@ -1,13 +1,17 @@
 includes:
   - layer:basic
   - layer:apt
+  - interface:nrpe-external-master
 options:
   basic:
     packages:
-      - python3-virtualenv
         # Install "juju" Python package build dependencies.
       - libssl-dev
       - libffi-dev
+    python_packages:
+      # do not put it in wheelhouse.txt, since its deps need compilaton
+      - juju==2.7.1
+    use_venv: false
   apt:
     packages:
       - juju-lint
diff --git a/metadata.yaml b/metadata.yaml
index 383b7b9..f1c0fc7 100644
--- a/metadata.yaml
+++ b/metadata.yaml
@@ -18,9 +18,10 @@ series:
   - xenial
   - trusty
 subordinate: false
-# provides:
-#   provides-relation:
-#     interface: interface-name
+provides:
+  nrpe-external-master:
+    interface: nrpe-external-master
+    scope: container
 # requires:
 #   requires-relation:
 #     interface: interface-name
diff --git a/reactive/charm_juju_lint.py b/reactive/charm_juju_lint.py
index c3ad41c..23004eb 100644
--- a/reactive/charm_juju_lint.py
+++ b/reactive/charm_juju_lint.py
@@ -16,16 +16,16 @@ this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
 import json
+import subprocess
 from os import (
-    getenv,
-    mkdir,
+    makedirs,
     path,
 )
-import virtualenv
 
 from charmhelpers.core import hookenv
 from charmhelpers.core.host import rsync
 from charmhelpers.fetch.python import packages
+from charmhelpers.contrib.charmsupport import nrpe
 from charms.reactive import (
     when,
     when_not,
@@ -34,35 +34,33 @@ from charms.reactive import (
 
 USR_LIB = "/usr/lib/juju-lint"
 VAR_LIB = "/var/lib/juju-lint"
+LINT_RESULTS_FILE = path.join(VAR_LIB, 'lint-results.txt')
+NAGIOS_PLUGINS_DIR = '/usr/local/lib/nagios/plugins/'
 
 
 @when_not("charm-juju-lint.installed")
 def install_charm_juju_lint():
     create_install_dirs()
-    virtualenv.create_environment(USR_LIB)
-    packages.pip_install("juju", venv=USR_LIB)
     deploy_scripts()
     hookenv.status_set("active", "Unit is ready")
     set_flag("charm-juju-lint.installed")
 
 
 def create_install_dirs():
-    install_dirs = (USR_LIB, VAR_LIB)
+    install_dirs = (USR_LIB, VAR_LIB, NAGIOS_PLUGINS_DIR)
     for install_dir in install_dirs:
-        if not path.isdir(install_dir):
-            try:
-                mkdir(install_dir)
-            except OSError as error:
-                print("{} directory creation failed: {}".format(
-                    install_dir, error
-                ))
+        makedirs(install_dir, exist_ok=True)
 
 
 def deploy_scripts():
-    rsync(
-        path.join(getenv("CHARM_DIR"), "scripts", "auto_lint.py"),
-        path.join(USR_LIB, "auto_lint.py")
-    )
+    # install scripts
+    charm_dir = hookenv.charm_dir()
+    # NOTE: the trailing slash(/) here is important for rsync
+    # which means all content in dir other than dir itself.
+    scripts_dir = path.join(charm_dir, 'scripts/')
+    rsync(path.join(scripts_dir, "auto_lint.py"), path.join(USR_LIB, "auto_lint.py"))
+    # install all scripts/check_xxx files into nagios plugins dir
+    rsync(scripts_dir, NAGIOS_PLUGINS_DIR, options=["--include='check_*'"])
 
 
 @when("charm-juju-lint.installed", "config.changed")
@@ -95,3 +93,34 @@ def create_crontab():
     )
     with open("/etc/cron.d/juju-lint", "w") as fp:
         fp.write(cron_job)
+
+    # call it now so we have a lint-results.txt
+    # for check_juju_lint_results.py to check against
+    # it also acts as a smoke test for the cron job
+    try:
+        subprocess.check_call([path.join(USR_LIB, "auto_lint.py")])
+    except subprocess.CalledProcessError as e:
+        hookenv.log('fail to run auto_lint.py: {}'.format(e), 'ERROR')
+        hookenv.status_set('blocked', 'fail to run auto_lint.py')
+
+
+@when_not("juju-lint.lint-results-file.available")
+def check_lint_results_file():
+    if path.isfile(LINT_RESULTS_FILE):
+        set_flag("juju-lint.lint-results-file.available")
+        hookenv.status_set('active', 'Unit is ready')
+
+
+@when("juju-lint.lint-results-file.available", "nrpe-external-master.available")
+def update_nrpe_checks():
+    nrpe_client = nrpe.NRPE()
+
+    shortname = 'juju_lint_results'
+    nrpe_client.add_check(
+        shortname=shortname,
+        description='check juju lint results file genrated by juju-lint',
+        # class nrpe.Check will convert cmd to full path.
+        check_cmd='check_juju_lint_results.py --lint-results-file {}'.format(LINT_RESULTS_FILE)
+    )
+    hookenv.log('check_{} added'.format(shortname))
+    nrpe_client.write()
diff --git a/scripts/auto_lint.py b/scripts/auto_lint.py
index 3aacb16..d553d6d 100755
--- a/scripts/auto_lint.py
+++ b/scripts/auto_lint.py
@@ -1,4 +1,4 @@
-#!/usr/lib/juju-lint/bin/python3
+#!/usr/bin/env python3
 
 """
 # auto_lint.py
@@ -24,6 +24,7 @@ from os import path
 from subprocess import (
     CalledProcessError,
     check_output,
+    STDOUT,
 )
 from sys import exit
 
@@ -65,12 +66,15 @@ def lint_juju(auto_lint_config):
         path.join(VAR_LIB, "juju-status.json")
     ]
     try:
-        lint_results = check_output(command)
+        # juju-lint uses python logging as output which is printing to stderr
+        # to get lint results, we need to redirect stderr to STDOUT
+        lint_results = check_output(command, stderr=STDOUT).decode('utf8')
     except CalledProcessError as error:
-        print("juju-lint failed with the following error:", error)
-    else:
-        with open(path.join(VAR_LIB, "lint-results.txt"), "w") as fp:
-            fp.write(lint_results)
+        # write error into lint results file, so nagios can know
+        lint_results = "ERROR: juju-lint failed: {}".format(error.output.decode('utf8'))
+
+    with open(path.join(VAR_LIB, "lint-results.txt"), "w") as fp:
+        fp.write(lint_results)
 
 
 def main():
diff --git a/scripts/check_juju_lint_results.py b/scripts/check_juju_lint_results.py
new file mode 100755
index 0000000..7d9caab
--- /dev/null
+++ b/scripts/check_juju_lint_results.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+
+"""
+# check_juju_lint_results.py
+
+Nagios plugin script to check juju lint results.
+
+Copyright (C) 2020 Canonical Ltd.
+Copyright (C) 2020 Joe Guo <joe.guo@xxxxxxxxxxxxx>
+
+This program is free software: you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3, as published by the Free
+Software Foundation.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License along with
+this program.  If not, see <https://www.gnu.org/licenses/>.
+"""
+
+import argparse
+import sys
+from os.path import join
+
+VAR_LIB = "/var/lib/juju-lint"
+LINT_RESULTS_FILE = join(VAR_LIB, 'lint-results.txt')
+
+NAGIOS_STATUS_OK = 0
+NAGIOS_STATUS_WARNING = 1
+NAGIOS_STATUS_CRITICAL = 2
+NAGIOS_STATUS_UNKNOWN = 3
+
+NAGIOS_STATUS = {
+    NAGIOS_STATUS_OK: 'OK',
+    NAGIOS_STATUS_WARNING: 'WARNING',
+    NAGIOS_STATUS_CRITICAL: 'CRITICAL',
+    NAGIOS_STATUS_UNKNOWN: 'UNKNOWN',
+}
+
+
+def nagios_exit(status, message):
+    assert status in NAGIOS_STATUS, "Invalid Nagios status code"
+    # prefix status name to message
+    output = '{}: {}'.format(NAGIOS_STATUS[status], message)
+    print(output)  # nagios requires print to stdout, no stderr
+    sys.exit(status)
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description='check juju lint results',
+        # show default in help
+        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+
+    parser.add_argument(
+        '-f', '--lint-results-file',
+        type=argparse.FileType(mode='r'),
+        dest='lint_results_file',
+        default=LINT_RESULTS_FILE,
+        help='juju lint results file to check, use `-` for stdin')
+
+    args = parser.parse_args()
+    lint_results = args.lint_results_file.read()
+
+    if 'ERROR' in lint_results or 'CRITICAL' in lint_results:
+        message = 'juju-lint errors\n{}'.format(lint_results)
+        nagios_exit(NAGIOS_STATUS_CRITICAL, message)
+    elif 'WARNING' in lint_results:
+        message = 'juju-lint warnings\n{}'.format(lint_results)
+        nagios_exit(NAGIOS_STATUS_WARNING, message)
+    else:
+        message = 'juju lint is happy\n{}'.format(lint_results)
+        nagios_exit(NAGIOS_STATUS_OK, message)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/test-requirements.txt b/test-requirements.txt
new file mode 100644
index 0000000..3930480
--- /dev/null
+++ b/test-requirements.txt
@@ -0,0 +1 @@
+flake8
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..4e4d2a2
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,17 @@
+[tox]
+envlist = py3,pep8
+skipsdist = True
+
+[testenv]
+basepython = python3
+deps =
+    -rwheelhouse.txt
+    -rtest-requirements.txt
+
+[testenv:py3]
+commands =
+    python -m unittest unit_tests.test_juju_lint
+
+[testenv:pep8]
+commands =
+    python -m flake8 --verbose --max-line-length=120
diff --git a/unit_tests/__init__.py b/unit_tests/__init__.py
new file mode 100644
index 0000000..958e747
--- /dev/null
+++ b/unit_tests/__init__.py
@@ -0,0 +1,5 @@
+#!/usr/bin/env python3
+import sys
+from os.path import abspath, dirname
+CURRENT_DIR = dirname(abspath(__file__))
+sys.path.insert(0, CURRENT_DIR)
diff --git a/unit_tests/lint_results/CRITICAL.txt b/unit_tests/lint_results/CRITICAL.txt
new file mode 100644
index 0000000..3e29a1e
--- /dev/null
+++ b/unit_tests/lint_results/CRITICAL.txt
@@ -0,0 +1,3 @@
+CRITICAL: this is critical
+ERROR: this is error
+WARNING: this is warning
diff --git a/unit_tests/lint_results/ERROR.txt b/unit_tests/lint_results/ERROR.txt
new file mode 100644
index 0000000..3103101
--- /dev/null
+++ b/unit_tests/lint_results/ERROR.txt
@@ -0,0 +1,4 @@
+ERROR: this is error
+WARNING: this is warning
+INFO: this is info
+DEBUG: this is debug
diff --git a/unit_tests/lint_results/INFO.txt b/unit_tests/lint_results/INFO.txt
new file mode 100644
index 0000000..1896f76
--- /dev/null
+++ b/unit_tests/lint_results/INFO.txt
@@ -0,0 +1,2 @@
+INFO: this is info
+DEBUG: this is debug
diff --git a/unit_tests/lint_results/WARNING.txt b/unit_tests/lint_results/WARNING.txt
new file mode 100644
index 0000000..aa26f36
--- /dev/null
+++ b/unit_tests/lint_results/WARNING.txt
@@ -0,0 +1,3 @@
+WARNING: this is warning
+INFO: this is info
+DEBUG: this is debug
diff --git a/unit_tests/test_juju_lint.py b/unit_tests/test_juju_lint.py
new file mode 100755
index 0000000..44925e9
--- /dev/null
+++ b/unit_tests/test_juju_lint.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+
+import unittest
+import subprocess
+from os.path import abspath, dirname, join
+
+CURRENT_DIR = dirname(abspath(__file__))
+CHARM_DIR = dirname(CURRENT_DIR)
+SCRIPT_DIR = join(CHARM_DIR, 'scripts')
+LINT_RESULTS_DIR = join(CURRENT_DIR, 'lint_results')
+CHECK_JUJU_LINT_RESULTS_PATH = join(SCRIPT_DIR, 'check_juju_lint_results.py')
+
+
+def get_lint_results_file(name):
+    """
+    Get full file path from name.
+
+    ERROR.txt --> /path/to/unit_tests/lint_results/ERROR.txt
+    """
+    return join(LINT_RESULTS_DIR, name)
+
+
+def get_check_exit_code(name):
+    """
+    Return check script exit code for a file name.
+    """
+
+    cmd = '{} --lint-results-file {}'.format(
+        CHECK_JUJU_LINT_RESULTS_PATH, get_lint_results_file(name))
+    try:
+        subprocess.check_call(cmd.split())
+    except subprocess.CalledProcessError as e:
+        return e.returncode
+    return 0
+
+
+class CheckScriptTestCase(unittest.TestCase):
+
+    def test_info(self):
+        self.assertEqual(get_check_exit_code('INFO.txt'), 0)
+
+    def test_warning(self):
+        self.assertEqual(get_check_exit_code('WARNING.txt'), 1)
+
+    def test_error(self):
+        self.assertEqual(get_check_exit_code('ERROR.txt'), 2)
+
+    def test_critial(self):
+        self.assertEqual(get_check_exit_code('CRITICAL.txt'), 2)
diff --git a/wheelhouse.txt b/wheelhouse.txt
new file mode 100644
index 0000000..2543182
--- /dev/null
+++ b/wheelhouse.txt
@@ -0,0 +1,9 @@
+# layer-basic/wheelhouse.txt already have these
+# but too old, override with new versions
+charms.reactive==1.3.0
+charmhelpers==0.20.7
+
+# this is the pip package for python-libjuju, required by charm-juju-lint
+# its deps need compilation so we have to exclude it from wheelhouse.txt
+# instead, we list it under layer-basic:python_packages option in layer.yaml
+# juju==2.7.1

Follow ups