diff --git a/README.md b/README.md index cb3d38c..41a6bb7 100644 --- a/README.md +++ b/README.md @@ -1,114 +1,144 @@ # Platform checks ## Introduction Allow to perform checks on a PaaS. This is built for the Nasqueron servers infrastructure, and will be useful on modern platform installations like microservices, Docker and proxies/load balancers. Each check is compatible Nagios / NRPE. ## Configuration Checks can be defined in `/etc/monitoring/checks.yml`. The format of this file is: ```yaml checks: <check_type>: <key>: <value> ``` You can also use `/usr/local/etc/monitoring/checks.yml` as path, or `.checks.yml` in the current working directory. ## Checks reference ### check_http_200 Perform a HTTP request to a specific URL. The check success if HTTP code is 200. #### Available check types * check_http_200: for sites you test at the back-end level * check_http_200_alive: for sites returning "ALIVE" as body content * check_http_200_proxy: flag site as verified at proxy level, not directly at the back-end * check_http_200_alive_proxy: combine proxy and alive flags #### Configuration example ```yaml checks: check_http_200: acme: http://localhost:41080/health cachet: http://localhost:39080/api/v1/ping check_http_200_proxy: openfire: https://xmpp.nasqueron.org/login.jsp check_http_200_alive_proxy: phabricator: https://devcentral.nasqueron.org/status ``` #### Run the check From there, you've two ways to run the check: * one check for all URLs: call `check_http_200` without argument * one check, one URL: call `check_http_200 <service name>`, for example `check_http_200 openfire` You can also create a check calling `check_http_200` without argument, and it will test every site. ### check_container_present #### Run the check With argument, check if the specified Docker container is running: `check_container_present foo` Without argument, compare the list of containers present with the expected one. The configuration is only required if you use it without argument. #### Available check types * check_docker_containers: a list of expected containers #### Configuration example ```yaml checks: check_docker_containers: - foo - bar ``` #### Requirements A Docker engine with CLI restructured, ie Docker 1.13+, is needed: * To list the containers it uses `docker container ls`. * To get more info on a container down, it uses `docker container inspect`. #### Not features This check isn't intended to detect containers run with other engines like `systemd-nspawn`. +### check_software_version + +#### Run the check + +With argument, check if the specified software is up-to-date. + +Without argument, get a list of software from the configuration. + +If the software can't be found, an UNKNOWN exit code is returned. + +#### Available check types + +* check_software_version: a list of software to keep up-to-date + +#### Configuration example + +```yaml +checks: + check_software_version: + - consul + - nomad + - terraform + - vagrant + - vault +``` + +#### Supported software + +* HashiCorp CLI products with a "version" subcommand, like Consul, Vault, Vagrant or TerraForm. + ## Return values of checks The checks use the standard Nagios/NRPE exit codes: | Exit code | Description | |-----------|-------------------------------------------| | 0 | OK | | 1 | Warning | | 2 | Critical | | 3 | Unknown, for example a check config issue | As such they're compatible with Nagios, Incinga, Sensu, Shinken, etc. diff --git a/bin/check_software_version b/bin/check_software_version new file mode 100755 index 0000000..fcd87c6 --- /dev/null +++ b/bin/check_software_version @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 + +# ------------------------------------------------------------- +# Platform checks - Software version +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Project: Nasqueron +# Description: Check if softwareversions is up-to-date +# License: BSD-2-Clause +# ------------------------------------------------------------- + + +import sys + +from platformchecks import exitcode +from platformchecks.checks import SoftwareVersionCheck +from platformchecks.config import parse_config + + +# ------------------------------------------------------------- +# Configuration +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +def get_software_list(): + return parse_config().get("checks", {}).get("check_software_version", []) + + +# ------------------------------------------------------------- +# Application entry point +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +def run_all(software_list): + success = True + messages = [] + + for software in software_list: + check = SoftwareVersionCheck(software) + check_success, message = check.perform() + + success &= check_success + messages.append(f"{software} {message}") + + print("\n".join(messages)) + return exitcode.ok_or_critical(success) + + +def run(software): + check = SoftwareVersionCheck(software) + success, message = check.perform() + + print(message) + return exitcode.ok_or_critical(success) + + +if __name__ == "__main__": + argc = len(sys.argv) + + if argc < 2: + exitCode = run_all(get_software_list()) + else: + exitCode = run(sys.argv[1]) + + sys.exit(exitCode) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..73382d3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +PyYAML>=3.12,<7.0 +requests>=2.20,<3.0 +semver~=3.0.2 diff --git a/setup.cfg b/setup.cfg index 87c5f8d..2c21602 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,35 +1,37 @@ [metadata] name = platform-checks -version = 0.1.3 +version = 0.2.0 author = Sébastien Santoro author_email = dereckson@espace-win.org description = Platform checks NRPE / Nagios long_description = file: README.md long_description_content_type = text/markdown license = BSD-2-Clause license_files = LICENSE url = https://devcentral.nasqueron.org/source/platform-checks/ project_urls = Bug Tracker = https://devcentral.nasqueron.org/tag/monitoring_and_reporting/ classifiers = Programming Language :: Python :: 3 License :: OSI Approved :: BSD License Operating System :: OS Independent Environment :: Console Intended Audience :: System Administrators Topic :: System :: Monitoring [options] package_dir = = src packages = find: scripts = bin/check_container_present bin/check_http_200 + bin/check_software_version python_requires = >=3.6 install_requires = PyYAML>=3.12,<7.0 requests>=2.20,<3.0 + semver~=3.0.2 [options.packages.find] where = src diff --git a/src/platformchecks/checks/__init__.py b/src/platformchecks/checks/__init__.py index 831f764..de0a024 100644 --- a/src/platformchecks/checks/__init__.py +++ b/src/platformchecks/checks/__init__.py @@ -1,2 +1,3 @@ from .http import HttpCheck from .docker import DockerContainerCheck +from .software import SoftwareVersionCheck diff --git a/src/platformchecks/checks/software.py b/src/platformchecks/checks/software.py new file mode 100644 index 0000000..40fe850 --- /dev/null +++ b/src/platformchecks/checks/software.py @@ -0,0 +1,255 @@ +# ------------------------------------------------------------- +# Platform checks +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Project: Nasqueron +# Description: Check if a softwareversions is up-to-date +# License: BSD-2-Clause +# ------------------------------------------------------------- + + +from .softwareversions import HashiCorpSoftwareVersionCheck + + +class SoftwareVersionCheck: + def __init__(self, software): + self.software = software + + def perform(self): + check = get_software_version_check(self.software) + return check.perform() + + +def get_software_version_check(software): + group = get_software_group(software) + if group == "HashiCorp": + return HashiCorpSoftwareVersionCheck(software) + + raise RuntimeError(f"Unknown software group for {software}") + + +def get_software_group(software): + for group, candidates in SOFTWARE_GROUPS.items(): + for candidate in candidates: + if candidate == software: + return group + + +SOFTWARE_GROUPS = { + "HashiCorp": [ + "atlas-upload-cli", + "boundary", + "boundary-desktop", + "boundary-worker", + "consul", + "consul-api-gateway", + "consul-aws", + "consul-cni", + "consul-dataplane", + "consul-ecs", + "consul-esm", + "consul-k8s", + "consul-k8s-control-plane", + "consul-lambda-extension", + "consul-lambda-registrator", + "consul-replicate", + "consul-template", + "consul-terraform-sync", + "docker-base", + "docker-basetool", + "envconsul", + "hc-install", + "hcdiag", + "hcs", + "levant", + "nomad", + "nomad-autoscaler", + "nomad-device-nvidia", + "nomad-driver-ecs", + "nomad-driver-lxc", + "nomad-driver-podman", + "nomad-pack", + "otto", + "packer", + "sentinel", + "serf", + "terraform", + "terraform-ls", + "terraform-provider-aci", + "terraform-provider-acme", + "terraform-provider-ad", + "terraform-provider-akamai", + "terraform-provider-alicloud", + "terraform-provider-archive", + "terraform-provider-arukas", + "terraform-provider-atlas", + "terraform-provider-auth0", + "terraform-provider-avi", + "terraform-provider-aviatrix", + "terraform-provider-aws", + "terraform-provider-awscc", + "terraform-provider-azure", + "terraform-provider-azuread", + "terraform-provider-azuredevops", + "terraform-provider-azurerm", + "terraform-provider-azurestack", + "terraform-provider-baiducloud", + "terraform-provider-bigip", + "terraform-provider-bitbucket", + "terraform-provider-boundary", + "terraform-provider-brightbox", + "terraform-provider-checkpoint", + "terraform-provider-chef", + "terraform-provider-cherryservers", + "terraform-provider-circonus", + "terraform-provider-ciscoasa", + "terraform-provider-clc", + "terraform-provider-cloudamqp", + "terraform-provider-cloudflare", + "terraform-provider-cloudinit", + "terraform-provider-cloudscale", + "terraform-provider-cloudstack", + "terraform-provider-cobbler", + "terraform-provider-cohesity", + "terraform-provider-constellix", + "terraform-provider-consul", + "terraform-provider-datadog", + "terraform-provider-digitalocean", + "terraform-provider-dme", + "terraform-provider-dns", + "terraform-provider-dnsimple", + "terraform-provider-docker", + "terraform-provider-dome9", + "terraform-provider-dyn", + "terraform-provider-ecl", + "terraform-provider-equinix", + "terraform-provider-exoscale", + "terraform-provider-external", + "terraform-provider-fakewebservices", + "terraform-provider-fastly", + "terraform-provider-flexibleengine", + "terraform-provider-fortios", + "terraform-provider-genymotion", + "terraform-provider-github", + "terraform-provider-gitlab", + "terraform-provider-google", + "terraform-provider-google-beta", + "terraform-provider-googleworkspace", + "terraform-provider-grafana", + "terraform-provider-gridscale", + "terraform-provider-hcloud", + "terraform-provider-hcp", + "terraform-provider-hcs", + "terraform-provider-hedvig", + "terraform-provider-helm", + "terraform-provider-heroku", + "terraform-provider-http", + "terraform-provider-icinga2", + "terraform-provider-ignition", + "terraform-provider-incapsula", + "terraform-provider-influxdb", + "terraform-provider-infoblox", + "terraform-provider-jdcloud", + "terraform-provider-ksyun", + "terraform-provider-kubernetes", + "terraform-provider-kubernetes-alpha", + "terraform-provider-lacework", + "terraform-provider-launchdarkly", + "terraform-provider-librato", + "terraform-provider-linode", + "terraform-provider-local", + "terraform-provider-logentries", + "terraform-provider-logicmonitor", + "terraform-provider-mailgun", + "terraform-provider-metalcloud", + "terraform-provider-mongodbatlas", + "terraform-provider-mso", + "terraform-provider-mysql", + "terraform-provider-ncloud", + "terraform-provider-netlify", + "terraform-provider-newrelic", + "terraform-provider-nomad", + "terraform-provider-ns1", + "terraform-provider-nsxt", + "terraform-provider-null", + "terraform-provider-nutanix", + "terraform-provider-oci", + "terraform-provider-okta", + "terraform-provider-oktaasa", + "terraform-provider-oneandone", + "terraform-provider-onelogin", + "terraform-provider-opc", + "terraform-provider-opennebula", + "terraform-provider-openstack", + "terraform-provider-opentelekomcloud", + "terraform-provider-opsgenie", + "terraform-provider-oraclepaas", + "terraform-provider-ovh", + "terraform-provider-packet", + "terraform-provider-pagerduty", + "terraform-provider-panos", + "terraform-provider-postgresql", + "terraform-provider-powerdns", + "terraform-provider-prismacloud", + "terraform-provider-profitbricks", + "terraform-provider-pureport", + "terraform-provider-rabbitmq", + "terraform-provider-rancher", + "terraform-provider-rancher2", + "terraform-provider-random", + "terraform-provider-rightscale", + "terraform-provider-rubrik", + "terraform-provider-rundeck", + "terraform-provider-runscope", + "terraform-provider-salesforce", + "terraform-provider-scaleway", + "terraform-provider-sdm", + "terraform-provider-selectel", + "terraform-provider-selvpc", + "terraform-provider-signalfx", + "terraform-provider-skytap", + "terraform-provider-softlayer", + "terraform-provider-spotinst", + "terraform-provider-stackpath", + "terraform-provider-statuscake", + "terraform-provider-sumologic", + "terraform-provider-telefonicaopencloud", + "terraform-provider-template", + "terraform-provider-tencentcloud", + "terraform-provider-terraform", + "terraform-provider-tfcoremock", + "terraform-provider-tfe", + "terraform-provider-thunder", + "terraform-provider-time", + "terraform-provider-tls", + "terraform-provider-triton", + "terraform-provider-turbot", + "terraform-provider-ucloud", + "terraform-provider-ultradns", + "terraform-provider-vault", + "terraform-provider-vcd", + "terraform-provider-venafi", + "terraform-provider-vmc", + "terraform-provider-vra", + "terraform-provider-vra7", + "terraform-provider-vsphere", + "terraform-provider-vthunder", + "terraform-provider-vultr", + "terraform-provider-wavefront", + "terraform-provider-yandex", + "tfc-agent", + "vagrant", + "vagrant-vmware-utility", + "vault", + "vault-auditor", + "vault-csi-provider", + "vault-k8s", + "vault-lambda-extension", + "vault-mssql-ekm-provider", + "vault-pkcs11-provider", + "vault-plugin-database-oracle", + "vault-servicenow-credential-resolver", + "vault-ssh-helper", + "waypoint", + "waypoint-entrypoint", + ] +} diff --git a/src/platformchecks/checks/softwareversions/__init__.py b/src/platformchecks/checks/softwareversions/__init__.py new file mode 100644 index 0000000..8458202 --- /dev/null +++ b/src/platformchecks/checks/softwareversions/__init__.py @@ -0,0 +1 @@ +from .hashicorp import HashiCorpSoftwareVersionCheck diff --git a/src/platformchecks/checks/softwareversions/hashicorp.py b/src/platformchecks/checks/softwareversions/hashicorp.py new file mode 100644 index 0000000..1fec586 --- /dev/null +++ b/src/platformchecks/checks/softwareversions/hashicorp.py @@ -0,0 +1,83 @@ +# ------------------------------------------------------------- +# Platform checks +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Project: Nasqueron +# Description: Check if a softwareversions is up-to-date +# License: BSD-2-Clause +# ------------------------------------------------------------- + + +import re +import requests +import subprocess + +from semver import VersionInfo as Version + + +class HashiCorpSoftwareVersionCheck: + def __init__(self, software, executable_name=None): + self.software = software + if executable_name: + self.executable_name = executable_name + else: + self.executable_name = software + + def perform(self): + _, last = self.get_last_version() + current = self.get_current_version() + + last = Version.parse(last) + current = Version.parse(current) + + if current < last: + return False, f"can be upgraded from {current} to {last}" + + return True, "up-to-date" + + def get_last_version(self, rc=False): + url = f"https://releases.hashicorp.com/{self.software}/" + response = requests.get(url) + + if response.status_code != 200: + return False, None + + lines = [ + line.strip() + for line in response.text.split("\n") + if f"/{self.software}/" in line + ] + + try: + for line in lines: + version = self.extract_published_version(line) + + if "+" in version: + continue + + if rc or not is_beta(version): + return True, version + except RuntimeError: + return False, None + + def extract_published_version(self, expression): + result = re.findall(r"\/([a-z0-9\-]+)\/(.*?)\/", expression) + if len(result) != 1: + raise RuntimeError("Can't extract version") + + software, version = result[0] + if software != self.software: + raise RuntimeError("Unexpected software name") + + return version + + def get_current_version(self): + p = subprocess.run([self.executable_name, "version"], capture_output=True) + if p.returncode != 0: + raise RuntimeError("Can't get executable version") + + expression = p.stdout.decode("UTF-8").strip() + return expression.split()[1][1:] + + +def is_beta(version): + return "rc" in version or "beta" in version