From 8120b2583c459c85116d69211038e0110d71e5b7 Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Wed, 1 Nov 2017 15:05:30 +0800 Subject: Add compass-tasks Registered tasks and MQ modules for Compass Change-Id: Id1569a61fe53357d53448478d5ba42cb1f386bc6 Signed-off-by: Harry Huang --- compass-tasks/actions/health_check/__init__.py | 13 ++ compass-tasks/actions/health_check/base.py | 57 ++++++ compass-tasks/actions/health_check/check.py | 96 +++++++++ compass-tasks/actions/health_check/check_apache.py | 89 +++++++++ compass-tasks/actions/health_check/check_celery.py | 115 +++++++++++ compass-tasks/actions/health_check/check_dhcp.py | 184 +++++++++++++++++ compass-tasks/actions/health_check/check_dns.py | 139 +++++++++++++ compass-tasks/actions/health_check/check_hds.py | 97 +++++++++ compass-tasks/actions/health_check/check_misc.py | 219 +++++++++++++++++++++ .../actions/health_check/check_os_installer.py | 151 ++++++++++++++ .../health_check/check_package_installer.py | 68 +++++++ compass-tasks/actions/health_check/check_squid.py | 128 ++++++++++++ compass-tasks/actions/health_check/check_tftp.py | 96 +++++++++ compass-tasks/actions/health_check/utils.py | 114 +++++++++++ 14 files changed, 1566 insertions(+) create mode 100644 compass-tasks/actions/health_check/__init__.py create mode 100644 compass-tasks/actions/health_check/base.py create mode 100644 compass-tasks/actions/health_check/check.py create mode 100644 compass-tasks/actions/health_check/check_apache.py create mode 100644 compass-tasks/actions/health_check/check_celery.py create mode 100644 compass-tasks/actions/health_check/check_dhcp.py create mode 100644 compass-tasks/actions/health_check/check_dns.py create mode 100644 compass-tasks/actions/health_check/check_hds.py create mode 100644 compass-tasks/actions/health_check/check_misc.py create mode 100644 compass-tasks/actions/health_check/check_os_installer.py create mode 100644 compass-tasks/actions/health_check/check_package_installer.py create mode 100644 compass-tasks/actions/health_check/check_squid.py create mode 100644 compass-tasks/actions/health_check/check_tftp.py create mode 100644 compass-tasks/actions/health_check/utils.py (limited to 'compass-tasks/actions/health_check') diff --git a/compass-tasks/actions/health_check/__init__.py b/compass-tasks/actions/health_check/__init__.py new file mode 100644 index 0000000..4ee55a4 --- /dev/null +++ b/compass-tasks/actions/health_check/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2014 Huawei Technologies Co. Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/compass-tasks/actions/health_check/base.py b/compass-tasks/actions/health_check/base.py new file mode 100644 index 0000000..22b6fae --- /dev/null +++ b/compass-tasks/actions/health_check/base.py @@ -0,0 +1,57 @@ +# Copyright 2014 Huawei Technologies Co. Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Base class for Compass Health Check.""" +from compass.actions.health_check import utils as health_check_utils +from compass.db.api import adapter as adapter_api +from compass.utils import setting_wrapper as setting + + +class BaseCheck(object): + """health check base class.""" + + def __init__(self): + self.config = setting + self.code = 1 + self.messages = [] + self.dist, self.version, self.release = health_check_utils.get_dist() + adapter_api.load_adapters_internal() + self.os_installer = self._get_os_installer() + self.package_installer = self._get_package_installer() + + def _get_os_installer(self): + installer = adapter_api.OS_INSTALLERS.values()[0] + os_installer = {} + os_installer['name'] = health_check_utils.strip_name( + installer['name']) + os_installer.update(installer['settings']) + return os_installer + + def _get_package_installer(self): + package_installer = {} + installer = adapter_api.PACKAGE_INSTALLERS.values()[0] + package_installer = {} + package_installer['name'] = health_check_utils.strip_name( + installer['name']) + package_installer.update(installer['settings']) + return package_installer + + def _set_status(self, code, message): + """set status.""" + self.code = code + self.messages.append(message) + + def get_status(self): + """get status.""" + return (self.code, self.messages) diff --git a/compass-tasks/actions/health_check/check.py b/compass-tasks/actions/health_check/check.py new file mode 100644 index 0000000..c1adbc6 --- /dev/null +++ b/compass-tasks/actions/health_check/check.py @@ -0,0 +1,96 @@ +# Copyright 2014 Huawei Technologies Co. Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Main Entry Point of Compass Health Check.""" +from compass.actions.health_check import base +from compass.actions.health_check import check_apache +from compass.actions.health_check import check_celery +from compass.actions.health_check import check_dhcp +from compass.actions.health_check import check_dns +from compass.actions.health_check import check_hds +from compass.actions.health_check import check_misc +from compass.actions.health_check import check_os_installer +from compass.actions.health_check import check_package_installer +from compass.actions.health_check import check_squid +from compass.actions.health_check import check_tftp + + +class BootCheck(base.BaseCheck): + """health check for all components.""" + + def run(self): + """do health check.""" + status = {} + status['apache'] = self._check_apache() + status['celery'] = self._check_celery() + status['dhcp'] = self._check_dhcp() + status['dns'] = self._check_dns() + status['hds'] = self._check_hds() + status['os_installer'] = self._check_os_installer() + status['package_installer'] = self._check_package_installer() + status['squid'] = self._check_squid() + status['tftp'] = self._check_tftp() + status['other'] = self._check_misc() + + return status + + def _check_apache(self): + """do apache health check.""" + checker = check_apache.ApacheCheck() + return checker.run() + + def _check_celery(self): + """do celery health check.""" + checker = check_celery.CeleryCheck() + return checker.run() + + def _check_dhcp(self): + """do dhcp health check.""" + checker = check_dhcp.DhcpCheck() + return checker.run() + + def _check_dns(self): + """do dns health check.""" + checker = check_dns.DnsCheck() + return checker.run() + + def _check_hds(self): + """do hds health check.""" + checker = check_hds.HdsCheck() + return checker.run() + + def _check_os_installer(self): + """do os installer health check.""" + checker = check_os_installer.OsInstallerCheck() + return checker.run() + + def _check_package_installer(self): + """do package installer health check.""" + checker = check_package_installer.PackageInstallerCheck() + return checker.run() + + def _check_squid(self): + """do squid health check.""" + checker = check_squid.SquidCheck() + return checker.run() + + def _check_tftp(self): + """do tftp health check.""" + checker = check_tftp.TftpCheck() + return checker.run() + + def _check_misc(self): + """do misc health check.""" + checker = check_misc.MiscCheck() + return checker.run() diff --git a/compass-tasks/actions/health_check/check_apache.py b/compass-tasks/actions/health_check/check_apache.py new file mode 100644 index 0000000..294d6f9 --- /dev/null +++ b/compass-tasks/actions/health_check/check_apache.py @@ -0,0 +1,89 @@ +# Copyright 2014 Huawei Technologies Co. Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Health Check module for Apache service.""" + +import socket +import urllib2 + +from compass.actions.health_check import base +from compass.actions.health_check import utils as health_check_utils + + +class ApacheCheck(base.BaseCheck): + """apache server health check class.""" + NAME = "Apache Check" + + def run(self): + """do the healthcheck.""" + if self.dist in ("centos", "redhat", "fedora", "scientific linux"): + apache_service = 'httpd' + else: + apache_service = 'apache2' + self.check_apache_conf(apache_service) + print "[Done]" + self.check_apache_running(apache_service) + print "[Done]" + if self.code == 1: + self.messages.append( + "[%s]Info: Apache health check has completed. " + "No problems found, all systems go." % self.NAME) + return (self.code, self.messages) + + def check_apache_conf(self, apache_service): + """Validates if Apache settings. + + :param apache_service : service type of apache, os dependent. + e.g. httpd or apache2 + :type apache_service : string + + """ + print "Checking Apache Config......", + conf_err_msg = health_check_utils.check_path( + self.NAME, + "/etc/%s/conf.d/ods-server.conf" % apache_service) + if not conf_err_msg == "": + self._set_status(0, conf_err_msg) + + wsgi_err_msg = health_check_utils.check_path( + self.NAME, + '/var/www/compass/compass.wsgi') + if not wsgi_err_msg == "": + self._set_status(0, wsgi_err_msg) + + return True + + def check_apache_running(self, apache_service): + """Checks if Apache service is running on port 80.""" + + print "Checking Apache service......", + serv_err_msg = health_check_utils.check_service_running(self.NAME, + apache_service) + if not serv_err_msg == "": + self._set_status(0, serv_err_msg) + if 'http' != socket.getservbyport(80): + self._set_status( + 0, + "[%s]Error: Apache is not listening on port 80." + % self.NAME) + try: + html = urllib2.urlopen('http://localhost') + html.geturl() + except Exception: + self._set_status( + 0, + "[%s]Error: Apache is not listening on port 80." + % self.NAME) + + return True diff --git a/compass-tasks/actions/health_check/check_celery.py b/compass-tasks/actions/health_check/check_celery.py new file mode 100644 index 0000000..2d8d27c --- /dev/null +++ b/compass-tasks/actions/health_check/check_celery.py @@ -0,0 +1,115 @@ +# Copyright 2014 Huawei Technologies Co. Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Health Check module for Celery.""" +import commands +import os + +from celery.task.control import inspect + +from compass.actions.health_check import base +from compass.actions.health_check import utils as health_check_utils + + +class CeleryCheck(base.BaseCheck): + """celery health check class.""" + NAME = "Celery Check." + + def run(self): + """do health check.""" + self.check_compass_celery_setting() + print "[Done]" + self.check_celery_backend() + print "[Done]" + if self.code == 1: + self.messages.append("[%s]Info: Celery health check " + "has completed. No problems found, " + "all systems go." % self.NAME) + return (self.code, self.messages) + + def check_compass_celery_setting(self): + """Validates Celery settings.""" + + print "Checking Celery setting......", + setting_map = { + 'logfile': 'CELERY_LOGFILE', + 'configdir': 'CELERYCONFIG_DIR', + 'configfile': 'CELERYCONFIG_FILE', + } + unset = [] + res = health_check_utils.validate_setting('Celery', + self.config, + 'CELERY_LOGFILE') + if res is False: + unset.append(setting_map["logfile"]) + self._set_status(0, res) + + res = health_check_utils.validate_setting('Celery', + self.config, + 'CELERYCONFIG_DIR') + if res is False: + unset.append(setting_map["configdir"]) + self._set_status(0, res) + + res = health_check_utils.validate_setting('Celery', + self.config, + 'CELERYCONFIG_FILE') + if res is False: + unset.append(setting_map["configdir"]) + self._set_status(0, res) + + if len(unset) != 0: + self._set_status(0, + "[%s]Error: Unset celery settings: %s" + " in /etc/compass/setting" + % (self.NAME, ', '.join(item for item in unset))) + return True + + def check_celery_backend(self): + """Checks if Celery backend is running and configured properly.""" + + print "Checking Celery Backend......", + if 'celery worker' not in commands.getoutput('ps -ef'): + self._set_status(0, "[%s]Error: celery is not running" % self.NAME) + return True + + if not os.path.exists('/etc/compass/celeryconfig'): + self._set_status( + 0, + "[%s]Error: No celery config file found for Compass" + % self.NAME) + return True + + try: + insp = inspect() + celery_stats = inspect.stats(insp) + if not celery_stats: + self._set_status( + 0, + "[%s]Error: No running Celery workers were found." + % self.NAME) + except IOError as error: + self._set_status( + 0, + "[%s]Error: Failed to connect to the backend: %s" + % (self.NAME, str(error))) + from errno import errorcode + if ( + len(error.args) > 0 and + errorcode.get(error.args[0]) == 'ECONNREFUSED' + ): + self.messages.append( + "[%s]Error: RabbitMQ server isn't running" + % self.NAME) + return True diff --git a/compass-tasks/actions/health_check/check_dhcp.py b/compass-tasks/actions/health_check/check_dhcp.py new file mode 100644 index 0000000..e3bae1e --- /dev/null +++ b/compass-tasks/actions/health_check/check_dhcp.py @@ -0,0 +1,184 @@ +# Copyright 2014 Huawei Technologies Co. Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Health Check module for DHCP service.""" +import commands +import os +import re +import socket +import xmlrpclib + +from compass.actions.health_check import base + + +class DhcpCheck(base.BaseCheck): + """dhcp health check class.""" + + NAME = "DHCP Check" + + def run(self): + """do health check.""" + method_name = "self.check_" + self.os_installer['name'] + "_dhcp()" + return eval(method_name) + + def check_cobbler_dhcp(self): + """Checks if Cobbler has taken over DHCP service.""" + + try: + remote = xmlrpclib.Server( + self.os_installer['cobbler_url'], + allow_none=True) + credentials = self.os_installer['credentials'] + remote.login( + credentials['username'], credentials['password']) + except Exception: + self._set_status( + 0, + "[%s]Error: Cannot login to Cobbler with " + "the tokens provided in the config file" % self.NAME) + return (self.code, self.messages) + + cobbler_settings = remote.get_settings() + if cobbler_settings['manage_dhcp'] == 0: + self.messages.append( + "[%s]Info: DHCP service is " + "not managed by Compass" % self.NAME) + self.code = 0 + return (self.code, self.messages) + + self.check_cobbler_dhcp_template() + print "[Done]" + self.check_dhcp_service() + self.check_dhcp_netmask() + print "[Done]" + if self.code == 1: + self.messages.append( + "[%s]Info: DHCP health check has completed. " + "No problems found, all systems go." % self.NAME) + + return (self.code, self.messages) + + def check_cobbler_dhcp_template(self): + """Validates Cobbler's DHCP template file.""" + print "Checking DHCP template......", + if os.path.exists("/etc/cobbler/dhcp.template"): + var_map = { + "match_next_server": False, + "match_subnet": False, + "match_filename": False, + "match_range": False, + } + + ip_regex = re.compile(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$') + + dhcp_template = open("/etc/cobbler/dhcp.template") + for line in dhcp_template.readlines(): + if line.find("next_server") != -1: + elmlist = line.split(" ") + for elm in elmlist: + if ";" in elm: + elm = elm[:-2] + + if "$next_server" in elm or ip_regex.match(elm): + var_map["match_next_server"] = True + + elif line.find("subnet") != -1 and line.find("{") != -1: + elmlist = line.split(" ") + for elm in elmlist: + if ip_regex.match(elm): + if elm[-1] == "0" and "255" not in elm: + var_map["match_subnet"] = True + elif elm[-1] != "0": + self.messages.append( + "[%s]Error: Subnet should be set " + "in the form of 192.168.0.0 in" + "/etc/cobbler/dhcp.template" % self.NAME) + + elif line.find("filename") != -1: + var_map["match_filename"] = True + elif line.find("range dynamic-bootp") != -1: + elmlist = line.split(" ") + ip_count = 0 + for elm in elmlist: + if ";" in elm and "\n" in elm: + elm = elm[:-2] + + if ip_regex.match(elm): + ip_count += 1 + + if ip_count != 2: + self.messages.append( + "[%s]Error: DHCP range should be set " + "between two IP addresses in " + "/etc/cobbler/dhcp.template" % self.NAME) + else: + var_map["match_range"] = True + + dhcp_template.close() + fails = [] + for var in var_map.keys(): + if var_map[var] is False: + fails.append(var) + + if len(fails) != 0: + self._set_status( + 0, + "[%s]Info: DHCP template file " + "failed components: %s" % ( + self.NAME, ' '.join(failed for failed in fails))) + + else: + self._set_status( + 0, + "[%s]Error: DHCP template file doesn't exist, " + "health check failed." % self.NAME) + + return True + + def check_dhcp_netmask(self): + with open('/etc/dhcp/dhcpd.conf') as conf_reader: + lines = conf_reader.readlines() + for line in lines: + if re.search('^subnet', line): + elm_list = line.split(' ') + break + subnet_ip = elm_list[1] + netmask = elm_list[-2] + subnet_ip_elm = subnet_ip.split('.') + netmask_elm = netmask.split('.') + for index, digit in enumerate(subnet_ip_elm): + if int(digit) & int(netmask_elm[index]) != int(digit): + self._set_status( + 0, + "[%s]Info: DHCP subnet IP and " + "netmask do not match" % self.NAME) + break + return True + + def check_dhcp_service(self): + """Checks if DHCP is running on port 67.""" + print "Checking DHCP service......", + if not commands.getoutput('pgrep dhcp'): + self._set_status( + 0, + "[%s]Error: dhcp service does not " + "seem to be running" % self.NAME) + + if socket.getservbyport(67) != 'bootps': + self._set_status( + 0, + "[%s]Error: bootps is not listening " + "on port 67" % self.NAME) + + return True diff --git a/compass-tasks/actions/health_check/check_dns.py b/compass-tasks/actions/health_check/check_dns.py new file mode 100644 index 0000000..843d7e2 --- /dev/null +++ b/compass-tasks/actions/health_check/check_dns.py @@ -0,0 +1,139 @@ +# Copyright 2014 Huawei Technologies Co. Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Health Check module for DNS service.""" + +import commands +import os +import socket +import xmlrpclib + +from compass.actions.health_check import base + + +class DnsCheck(base.BaseCheck): + """dns health check class.""" + NAME = "DNS Check" + + def run(self): + """do health check.""" + method_name = "self.check_" + self.os_installer['name'] + "_dns()" + return eval(method_name) + + def check_cobbler_dns(self): + """Checks if Cobbler has taken over DNS service.""" + try: + remote = xmlrpclib.Server( + self.os_installer['cobbler_url'], + allow_none=True) + credentials = self.os_installer['credentials'] + remote.login( + credentials['username'], credentials['password']) + except Exception: + self._set_status(0, + "[%s]Error: Cannot login to Cobbler " + "with the tokens provided in the config file" + % self.NAME) + return (self.code, self.messages) + + cobbler_settings = remote.get_settings() + if cobbler_settings['manage_dns'] == 0: + self.messages.append('[DNS]Info: DNS is not managed by Compass') + return (0, self.messages) + self.check_cobbler_dns_template() + print "[Done]" + self.check_dns_service() + print "[Done]" + if self.code == 1: + self.messages.append( + "[%s]Info: DNS health check has complated. " + "No problems found, all systems go." % self.NAME) + return (self.code, self.messages) + + def check_cobbler_dns_template(self): + """Validates Cobbler's DNS template file.""" + + print "Checking DNS template......", + if os.path.exists("/etc/cobbler/named.template"): + var_map = { + "match_port": False, + "match_allow_query": False, + } + named_template = open("/etc/cobbler/named.template") + host_ip = socket.gethostbyname(socket.gethostname()) + missing_query = [] + for line in named_template.readlines(): + if "listen-on port 53" in line and host_ip in line: + var_map["match_port"] = True + + if "allow-query" in line: + for subnet in ["127.0.0.0/8"]: + if subnet not in line: + missing_query.append(subnet) + + named_template.close() + + if var_map["match_port"] is False: + self.messages.append( + "[%s]Error: named service port " + "and/or IP is misconfigured in " + "/etc/cobbler/named.template" % self.NAME) + + if len(missing_query) != 0: + self.messages.append( + "[%s]Error: Missing allow_query values in " + "/etc/cobbler/named.template:%s" % ( + self.NAME, + ', '.join(subnet for subnet in missing_query))) + else: + var_map["match_allow_query"] = True + + fails = [] + for var in var_map.keys(): + if var_map[var] is False: + fails.append(var) + + if len(fails) != 0: + self._set_status( + 0, + "[%s]Info: DNS template failed components: " + "%s" % ( + self.NAME, + ' '.join(failed for failed in fails))) + + else: + self._set_status( + 0, + "[%s]Error: named template file doesn't exist, " + "health check failed." % self.NAME) + + return True + + def check_dns_service(self): + """Checks if DNS is running on port 53.""" + + print "Checking DNS service......", + if 'named' not in commands.getoutput('ps -ef'): + self._set_status( + 0, + "[%s]Error: named service does not seem to be " + "running" % self.NAME) + + if socket.getservbyport(53) != 'domain': + self._set_status( + 0, + "[%s]Error: domain service is not listening on port " + "53" % self.NAME) + + return None diff --git a/compass-tasks/actions/health_check/check_hds.py b/compass-tasks/actions/health_check/check_hds.py new file mode 100644 index 0000000..d176f1f --- /dev/null +++ b/compass-tasks/actions/health_check/check_hds.py @@ -0,0 +1,97 @@ +# Copyright 2014 Huawei Technologies Co. Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Health Check module for Hardware Discovery.""" +import logging + +from compass.actions.health_check import base +from compass.actions.health_check import utils as health_check_utils + + +class HdsCheck(base.BaseCheck): + """hds health check class.""" + NAME = "HDS Check" + + def run(self): + """do health check.""" + if self.dist in ("centos", "redhat", "fedora", "scientific linux"): + pkg_type = "yum" + else: + pkg_type = "apt" + + try: + pkg_module = __import__(pkg_type) + except Exception: + self._set_status( + 0, "[%s]Error: No module named %s please install it first." + % (self.NAME, pkg_type) + ) + return (self.code, self.messages) + + logging.info('import %s: %s', pkg_type, pkg_module) + method_name = 'self.check_' + pkg_type + '_snmp(pkg_module)' + eval(method_name) + print "[Done]" + self.check_snmp_mibs() + print "[Done]" + if self.code == 1: + self.messages.append("[%s]Info: hds health check has complated. " + "No problems found, all systems go." + % self.NAME) + + return (self.code, self.messages) + + def check_yum_snmp(self, pkg_module): + """Check if SNMP yum dependencies are installed + + :param pkg_module : python yum library + :type pkg_module : python module + + """ + print "Checking SNMP Packages......", + yum_base = pkg_module.YumBase() + uninstalled = [] + for package in ['net-snmp-utils', 'net-snmp', 'net-snmp-python']: + if len(yum_base.rpmdb.searchNevra(name=package)) == 0: + self.messages.append("[%s]Error: %s package is required " + "for HDS" % (self.NAME, package)) + uninstalled.append(package) + + if len(uninstalled) != 0: + self._set_status(0, "[%s]Info: Uninstalled packages: %s" + % (self.NAME, + ', '.join(item for item in uninstalled))) + + return True + + def check_apt_snmp(self, pkg_module): + """do apt health check.""" + return None + + def check_snmp_mibs(self): + """Checks if SNMP MIB files are properly placed.""" + + print "Checking SNMP MIBs......", + conf_err_msg = health_check_utils.check_path(self.NAME, + '/etc/snmp/snmp.conf') + if not conf_err_msg == "": + self._set_status(0, conf_err_msg) + + mibs_err_msg = health_check_utils.check_path( + self.NAME, + '/usr/local/share/snmp/mibs') + if not mibs_err_msg == "": + self._set_status(0, mibs_err_msg) + + return True diff --git a/compass-tasks/actions/health_check/check_misc.py b/compass-tasks/actions/health_check/check_misc.py new file mode 100644 index 0000000..b8beb1b --- /dev/null +++ b/compass-tasks/actions/health_check/check_misc.py @@ -0,0 +1,219 @@ +# Copyright 2014 Huawei Technologies Co. Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Miscellaneous Health Check for Compass.""" +import logging + +from compass.actions.health_check import base +from compass.actions.health_check import utils as health_check_utils + + +class MiscCheck(base.BaseCheck): + """health check for misc.""" + NAME = "Miscellaneous Check" + + MISC_MAPPING = { + "yum": "rsyslog ntp iproute openssh-clients python git wget " + "python-setuptools " + "amqp mod_wsgi httpd squid " + "dhcp bind rsync yum-utils xinetd tftp-server gcc " + "net-snmp-utils net-snmp".split(" "), + "pip": "netaddr flask flask_script flask_restful amqplib " + "flask_sqlalchemy paramiko mock celery six discover daemon " + "unittest2 chef".split(" "), + "disable": "iptables ip6tables".split(" "), + "enable": "httpd squid xinetd dhcpd named sshd rsyslog cobblerd " + "ntpd compass-celeryd compass-progress-updated".split(" "), + } + + def run(self): + """do health check.""" + self.check_linux_dependencies() + print "[Done]" + self.check_pip_dependencies() + print "[Done]" + self.check_ntp() + print "[Done]" + self.check_rsyslogd() + print "[Done]" + self.check_chkconfig() + print "[Done]" + self.check_selinux() + print "[Done]" + + if self.code == 1: + self.messages.append( + "[%s]Info: Miscellaneous check has completed " + "No problems found, all systems go." % self.NAME) + return (self.code, self.messages) + + def check_linux_dependencies(self): + """Checks if dependencies are installed.""" + print "Checking Linux dependencies....", + if self.dist in ("centos", "redhat", "fedora", "scientific linux"): + pkg_type = "yum" + else: + pkg_type = "apt" + + try: + pkg_module = __import__(pkg_type) + except Exception: + self._set_status( + 0, + "[%s]Error: No module named %s, " + "please install it first." % (self.NAME, pkg_type)) + return True + + logging.info('import %s: %s', pkg_type, pkg_module) + method_name = 'self.check_' + pkg_type + '_dependencies(pkg_module)' + eval(method_name) + + def check_yum_dependencies(self, pkg_module): + """Checks if yum dependencies are installed. + + :param pkg_module : python yum library + :type pkg_module : python module + + """ + print "Checking Yum dependencies......", + yum_base = pkg_module.YumBase() + uninstalled = [] + for package in self.MISC_MAPPING["yum"]: + if len(yum_base.rpmdb.searchNevra(name=package)) == 0: + self._set_status( + 0, + "[%s]Error: %s package is required" + % (self.NAME, package)) + uninstalled.append(package) + + if len(uninstalled) != 0: + self._set_status( + 0, + "[%s]Info: Uninstalled yum packages: %s" + % (self.NAME, ', '.join(item for item in uninstalled))) + + return True + + def check_pip_dependencies(self): + """Checks if required pip packages are installed.""" + print "Checking pip dependencies......", + uninstalled = [] + for module in self.MISC_MAPPING['pip']: + try: + __import__(module) + except Exception: + self._set_status( + 0, + "[%s]Error: pip package %s is requred" + % (self.NAME, module)) + uninstalled.append(module) + + if len(uninstalled) != 0: + self._set_status( + 0, + "[%s]Info: Uninstalled pip packages: %s" + % (self.NAME, ', '.join(item for item in uninstalled))) + + return True + + def check_ntp(self): + """Validates ntp configuration and service.""" + + print "Checking NTP......", + conf_err_msg = health_check_utils.check_path(self.NAME, + '/etc/ntp.conf') + if not conf_err_msg == "": + self._set_status(0, conf_err_msg) + + serv_err_msg = health_check_utils.check_service_running(self.NAME, + 'ntpd') + if not serv_err_msg == "": + self._set_status(0, serv_err_msg) + + return True + + def check_rsyslogd(self): + """Validates rsyslogd configuration and service.""" + + print "Checking rsyslog......", + conf_err_msg = health_check_utils.check_path(self.NAME, + '/etc/rsyslog.conf') + if not conf_err_msg == "": + self._set_status(0, conf_err_msg) + + dir_err_msg = health_check_utils.check_path(self.NAME, + '/etc/rsyslog.d/') + if not dir_err_msg == "": + self._set_status(0, dir_err_msg) + + serv_err_msg = health_check_utils.check_service_running(self.NAME, + 'rsyslogd') + if not serv_err_msg == "": + self._set_status(0, serv_err_msg) + + return True + + def check_chkconfig(self): + """Check if required services are enabled on the start up.""" + + print "Checking chkconfig......", + serv_to_disable = [] + for serv in self.MISC_MAPPING["disable"]: + if health_check_utils.check_chkconfig(serv) is True: + self._set_status( + 0, + "[%s]Error: %s is not disabled" + % (self.NAME, serv)) + serv_to_disable.append(serv) + + if len(serv_to_disable) != 0: + self._set_status( + 0, + "[%s]Info: You need to disable these services " + "on system start-up: %s" + % (self.NAME, + ", ".join(item for item in serv_to_disable))) + + serv_to_enable = [] + for serv in self.MISC_MAPPING["enable"]: + if health_check_utils.check_chkconfig(serv) is False: + self._set_status( + 0, "[%s]Error: %s is disabled" % (self.NAME, serv)) + serv_to_enable.append(serv) + + if len(serv_to_enable) != 0: + self._set_status(0, "[%s]Info: You need to enable these " + "services on system start-up: %s" + % (self.NAME, + ", ".join(item for item in serv_to_enable))) + + return True + + def check_selinux(self): + """Check if SELinux is disabled.""" + print "Checking Selinux......", + disabled = False + with open("/etc/selinux/config") as selinux: + for line in selinux: + if "SELINUX=disabled" in line: + disabled = True + break + + if disabled is False: + self._set_status( + 0, + "[%s]Selinux is not disabled, " + "please disable it in /etc/selinux/config." % self.NAME) + + return True diff --git a/compass-tasks/actions/health_check/check_os_installer.py b/compass-tasks/actions/health_check/check_os_installer.py new file mode 100644 index 0000000..6ef9818 --- /dev/null +++ b/compass-tasks/actions/health_check/check_os_installer.py @@ -0,0 +1,151 @@ +# Copyright 2014 Huawei Technologies Co. Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Compass Health Check module for OS Installer.""" + +import os +import xmlrpclib + +from compass.actions.health_check import base + + +class OsInstallerCheck(base.BaseCheck): + """os installer health check.""" + NAME = "OS Installer Check" + + def run(self): + """do health check.""" + method_name = 'self.' + self.os_installer['name'] + '_check()' + return eval(method_name) + + def cobbler_check(self): + """Runs cobbler check from xmlrpc client.""" + try: + remote = xmlrpclib.Server( + self.os_installer['cobbler_url'], + allow_none=True) + credentials = self.os_installer['credentials'] + token = remote.login( + credentials['username'], credentials['password']) + except Exception: + self.code = 0 + self.messages.append( + "[%s]Error: Cannot login to Cobbler with " + "the tokens provided in the config file" + % self.NAME) + self.messages.append( + "[%s]Error: Failed to connect to Cobbler " + "API, please check if /etc/cobbler/setting " + "is properly configured" % self.NAME) + return (self.code, self.messages) + + check_result = remote.check(token) + + for index, message in enumerate(check_result): + if "SELinux" in message: + check_result.pop(index) + + if len(check_result) != 0: + self.code = 0 + for error_msg in check_result: + self.messages.append("[%s]Error: " % self.NAME + error_msg) + + if len(remote.get_distros()) == 0: + self._set_status(0, + "[%s]Error: No Cobbler distros found" % self.NAME) + + if len(remote.get_profiles()) == 0: + self._set_status(0, + "[%s]Error: No Cobbler profiles found" + % self.NAME) + + found_ppa = False + if len(remote.get_repos()) != 0: + for repo in remote.get_repos(): + if 'ppa_repo' in repo['mirror']: + found_ppa = True + break + + if found_ppa is False: + self._set_status(0, + "[%s]Error: No repository ppa_repo found" + % self.NAME) + + path_map = { + 'match_kickstart': ( + '/var/lib/cobbler/kickstarts/', + ['default.ks', 'default.seed'] + ), + 'match_snippets': ( + '/var/lib/cobbler/snippets/', + [ + 'kickstart_done', + 'kickstart_start', + 'kickstart_pre_partition_disks', + 'kickstart_partition_disks', + 'kickstart_pre_anamon', + 'kickstart_post_anamon', + 'kickstart_pre_install_network_config', + 'kickstart_network_config', + 'kickstart_post_install_network_config', + 'kickstart_chef', + 'kickstart_ntp', + 'kickstart_yum_repo_config', + 'preseed_pre_partition_disks', + 'preseed_partition_disks', + 'preseed_pre_anamon', + 'preseed_post_anamon', + 'preseed_pre_install_network_config', + 'preseed_network_config', + 'preseed_post_install_network_config', + 'preseed_chef', + 'preseed_ntp', + 'preseed_apt_repo_config', + ] + ), + 'match_ks_mirror': ( + '/var/www/cobbler/', + ['ks_mirror'] + ), + 'match_repo_mirror': ( + '/var/www/cobbler/', + ['repo_mirror'] + ), + 'match_iso': ( + '/var/lib/cobbler/', + ['iso'] + ), + } + not_exists = [] + for key in path_map.keys(): + for path in path_map[key][1]: + if not os.path.exists(path_map[key][0] + path): + not_exists.append(path_map[key][0] + path) + + if len(not_exists) != 0: + self._set_status( + 0, + "[%s]Error: These locations do not exist: " + "%s" % ( + self.NAME, + ', '.join(item for item in not_exists) + ) + ) + + if self.code == 1: + self.messages.append( + "[%s]Info: OS Installer health check has completed." + " No problems found, all systems go." % self.NAME) + + return (self.code, self.messages) diff --git a/compass-tasks/actions/health_check/check_package_installer.py b/compass-tasks/actions/health_check/check_package_installer.py new file mode 100644 index 0000000..efcd8e8 --- /dev/null +++ b/compass-tasks/actions/health_check/check_package_installer.py @@ -0,0 +1,68 @@ +# Copyright 2014 Huawei Technologies Co. Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Health Check module for Package Installer.""" +import logging +import os +import requests + +from compass.actions.health_check import base +from compass.actions.health_check import utils as health_check_utils + + +class PackageInstallerCheck(base.BaseCheck): + """package installer health check class.""" + NAME = "Package Installer Check" + + def run(self): + """do health check.""" + method_name = "self." + self.package_installer['name'] + "_check()" + return eval(method_name) + + def chef_check(self): + """Checks chef setting, cookbooks and roles.""" + self.check_chef_config_dir() + print "[Done]" + if self.code == 1: + self.messages.append( + "[%s]Info: Package installer health check " + "has completed. No problems found, all systems " + "go." % self.NAME) + + return (self.code, self.messages) + + def check_chef_config_dir(self): + """Validates chef configuration directories.""" + + print "Checking Chef configurations......", + message = health_check_utils.check_path(self.NAME, '/etc/chef-server/') + if not message == "": + self._set_status(0, message) + + message = health_check_utils.check_path(self.NAME, '/opt/chef-server/') + if not message == "": + self._set_status(0, message) + + return None + + def ansible_check(self): + """Placeholder for ansible check.""" + print "Checking ansible......" + print ("[Done]") + self.code == 1 + self.messages.append( + "[%s]Info: Package installer health check " + "has completed. No problems found, all systems " + "go." % self.NAME) + return (self.code, self.messages) diff --git a/compass-tasks/actions/health_check/check_squid.py b/compass-tasks/actions/health_check/check_squid.py new file mode 100644 index 0000000..5628a63 --- /dev/null +++ b/compass-tasks/actions/health_check/check_squid.py @@ -0,0 +1,128 @@ +# Copyright 2014 Huawei Technologies Co. Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Health Check module for Squid service.""" +import commands +import os +import pwd +import socket + +from compass.actions.health_check import base +from compass.actions.health_check import utils as health_check_utils + + +class SquidCheck(base.BaseCheck): + """Squid health check class.""" + NAME = "Squid Check" + + def run(self): + """do health check.""" + self.check_squid_files() + print "[Done]" + self.check_squid_service() + print "[Done]" + if self.code == 1: + self.messages.append( + "[%s]Info: Squid health check has completed. " + "No problems found, all systems go." % self.NAME) + return (self.code, self.messages) + + def check_squid_files(self): + """Validates squid config, cache directory and ownership.""" + print "Checking Squid Files......", + var_map = { + 'match_squid_conf': False, + 'match_squid_cache': False, + 'match_squid_ownership': False, + } + + conf_err_msg = health_check_utils.check_path( + self.NAME, + "/etc/squid/squid.conf") + if not conf_err_msg == "": + self._set_status(0, conf_err_msg) + elif int(oct(os.stat('/etc/squid/squid.conf').st_mode)) < 100644: + self._set_status( + 0, + "[%s]Error: squid.conf has incorrect " + "file permissions" % self.NAME) + else: + var_map['match_squid_conf'] = True + + squid_path_err_msg = health_check_utils.check_path( + self.NAME, '/var/squid/') + if not squid_path_err_msg == "": + self._set_status(0, squid_path_err_msg) + elif health_check_utils.check_path( + self.NAME, + '/var/squid/cache' + ) != "": + self._set_status( + 0, + health_check_utils.check_path( + self.NAME, + '/var/squid/cache' + ) + ) + else: + var_map['match_squid_cache'] = True + uid = os.stat('/var/squid/').st_uid + gid = os.stat('/var/squid/').st_gid + if uid != gid or pwd.getpwuid(23).pw_name != 'squid': + self._set_status( + 0, + "[%s]Error: /var/squid directory ownership " + "misconfigured" % self.NAME) + else: + var_map['match_squid_ownership'] = True + + fails = [] + for key in var_map.keys(): + if var_map[key] is False: + fails.append(key) + + if len(fails) != 0: + self.messages.append( + "[%s]Info: Failed components for squid config: " + "%s" % ( + self.NAME, + ', '.join(item for item in fails) + ) + ) + return True + + def check_squid_service(self): + """Checks if squid is running on port 3128.""" + + print "Checking Squid service......", + if 'squid' not in commands.getoutput('ps -ef'): + self._set_status( + 0, + "[%s]Error: squid service does not seem " + "running" % self.NAME) + + try: + if 'squid' != socket.getservbyport(3128): + self._set_status( + 0, + "[%s]Error: squid is not listening on " + "3128" % self.NAME) + + except Exception: + self._set_status( + 0, + "[%s]Error: No service is listening on 3128, " + "squid failed" % self.NAME) + + return True diff --git a/compass-tasks/actions/health_check/check_tftp.py b/compass-tasks/actions/health_check/check_tftp.py new file mode 100644 index 0000000..7ca6405 --- /dev/null +++ b/compass-tasks/actions/health_check/check_tftp.py @@ -0,0 +1,96 @@ +# Copyright 2014 Huawei Technologies Co. Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Health Check module for TFTP service.""" +import os +import socket +import xmlrpclib + +from compass.actions.health_check import base +from compass.actions.health_check import utils as health_check_utils + + +class TftpCheck(base.BaseCheck): + """tftp health check class.""" + NAME = "TFTP Check" + + def run(self): + """do health check.""" + method_name = "self.check_" + self.os_installer['name'] + "_tftp()" + return eval(method_name) + + def check_cobbler_tftp(self): + """Checks if Cobbler manages TFTP service. + + :note: we assume TFTP service is running at the + same machine where this health check runs at + """ + + try: + remote = xmlrpclib.Server( + self.os_installer['cobbler_url'], + allow_none=True) + credentials = self.os_installer['credentials'] + remote.login( + credentials['username'], credentials['password']) + except Exception: + self._set_status( + 0, + "[%s]Error: Cannot login to Cobbler with the tokens " + " provided in the config file" % self.NAME) + return (self.code, self.messages) + + cobbler_settings = remote.get_settings() + if cobbler_settings['manage_tftp'] == 0: + self.messages.append( + '[TFTP]Info: tftp service is not managed by Compass') + return (0, self.messages) + self.check_tftp_dir() + print "[Done]" + self.check_tftp_service() + print "[Done]" + if self.code == 1: + self.messages.append( + "[%s]Info: tftp service health check has completed. " + "No problems found, all systems go." % self.NAME) + + return (self.code, self.messages) + + def check_tftp_dir(self): + """Validates TFTP directories and configurations.""" + print "Checking TFTP directories......", + if not os.path.exists('/var/lib/tftpboot/'): + self._set_status( + 0, + "[%s]Error: No tftp-boot libraries found, " + "please check if tftp server is properly " + "installed/managed" % self.NAME) + + return True + + def check_tftp_service(self): + """Checks if TFTP is running on port 69.""" + print "Checking TFTP services......", + serv_err_msg = health_check_utils.check_service_running(self.NAME, + 'xinetd') + if not serv_err_msg == "": + self._set_status(0, serv_err_msg) + + if 'tftp' != socket.getservbyport(69): + self._set_status( + 0, + "[%s]Error: tftp doesn't seem to be listening " + "on Port 60." % self.NAME) + + return True diff --git a/compass-tasks/actions/health_check/utils.py b/compass-tasks/actions/health_check/utils.py new file mode 100644 index 0000000..369c5b6 --- /dev/null +++ b/compass-tasks/actions/health_check/utils.py @@ -0,0 +1,114 @@ +# Copyright 2014 Huawei Technologies Co. Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Compass Health Check heavy-lifting utilities""" +import commands +import os +import platform +import re + + +def validate_setting(module, setting, param): + """Checks if a Compass setting exists in the config file. + + :param module : module name to be checked + :type module : string + :param setting : compass setting wrapper + :type setting : python module + :param param : settings defined in compass config file + :type param : string + + """ + if hasattr(setting, param): + return True + else: + err_msg = "[%s]Error: no %s defined" % (module, param) + return err_msg + + +def get_dist(): + """Returns the operating system related information.""" + + os_version, version, release = platform.linux_distribution() + return (os_version.lower().strip(), version, release.lower().strip()) + + +def check_path(module_name, path): + """Checks if a directory or file exisits. + + :param module_name : module name to be checked + :type module_name : string + :param path : path of the directory of file + :type path : string + + """ + err_msg = "" + if not os.path.exists(path): + err_msg = ( + "[%s]Error: %s does not exist, " + "please check your configurations.") % (module_name, path) + return err_msg + + +def check_service_running(module_name, service_name): + """Checks if a certain service is running. + + :param module_name : module name to be checked + :type module_name : string + :param service_name : service name to be checked + :type service_name : string + + """ + err_msg = "" + if service_name not in commands.getoutput('ps -ef'): + err_msg = "[%s]Error: %s is not running." % ( + module_name, service_name) + + return err_msg + + +def check_chkconfig(service_name): + """Checks if a service is enabled at the start up. + + :param service_name : service name to be checked + :type service_name : string + + """ + chk_on = False + for service in os.listdir('/etc/rc3.d/'): + if service_name in service and 'S' in service: + chk_on = True + break + + return chk_on + + +def strip_name(name): + """Reformats names.""" + if not any([s in name for s in "(,),-,_".split(',')]): + return name + + paren_regex = re.compile("(.*?)\s*\(") + dash_regex = re.compile("(.*?)\s*\-") + under_dash_regex = re.compile("(.*?)\s*\_") + + r1 = paren_regex.match(name) + r2 = dash_regex.match(name) + r3 = under_dash_regex.match(name) + shortest = 'AVeryLongStringForDefualt' + for r in [r1, r2, r3]: + if r and len(r.group(1)) < len(shortest): + shortest = r.group(1) + + return shortest -- cgit 1.2.3-korg