From 5b02232aaa5e8b30a6536787cd575cfa0f0b8b9c Mon Sep 17 00:00:00 2001 From: Koji Shimizu Date: Sun, 23 Oct 2022 10:49:05 +0900 Subject: [PATCH] Prometheus rule file syntax test This is improvement of functional test of prometheus plugin. By this fix, the created prometheus rule files are checked by promtool. If the syntax of them are invalid, the test fails. This check is disabled by default and enabled when CONF.prometheus_plugin.test_rule_with_promtool is true. Implements: blueprint support-auto-lcm Change-Id: Ica331befe225156c607d5c8267462b7281669c91 --- .zuul.yaml | 7 +- .../user/prometheus_plugin_use_case_guide.rst | 11 +++ tacker/sol_refactored/common/config.py | 3 + .../common/prometheus_plugin.py | 81 +++++++++++++++--- .../conductor/prometheus_plugin_driver.py | 2 +- tacker/sol_refactored/controller/vnfpm_v2.py | 1 + .../samples/tacker-monitoring-test.zip | Bin 4873 -> 6366 bytes 7 files changed, 87 insertions(+), 18 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index f960f43df..6cda1a456 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -586,9 +586,10 @@ v2_vnfm: kubernetes_vim_rsc_wait_timeout: 800 prometheus_plugin: - fault_management: True - performance_management: True - auto_scaling: True + fault_management: true + performance_management: true + auto_scaling: true + test_rule_with_promtool: true tox_envlist: dsvm-functional-sol-kubernetes-v2 vars: prometheus_setup: true diff --git a/doc/source/user/prometheus_plugin_use_case_guide.rst b/doc/source/user/prometheus_plugin_use_case_guide.rst index 3aa4fb29d..2f6a52b28 100644 --- a/doc/source/user/prometheus_plugin_use_case_guide.rst +++ b/doc/source/user/prometheus_plugin_use_case_guide.rst @@ -52,6 +52,9 @@ performance_management, fault_management or auto_scaling below. * - ``CONF.prometheus_plugin.auto_scaling`` - false - Enable prometheus plugin autoscaling. + * - ``CONF.prometheus_plugin.test_rule_with_promtool`` + - false + - Enable rule file validation using promtool. System ~~~~~~ @@ -241,6 +244,14 @@ needs to activate sshd. - The directory indicated by "rule_files" setting of prometheus server config should be accessible by SSH. +Supported versions +------------------ + +Tacker Zed release + +- Prometheus: 2.37 +- Alertmanager: 0.24 + Alert rule registration ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tacker/sol_refactored/common/config.py b/tacker/sol_refactored/common/config.py index cd2be997e..456092ec5 100644 --- a/tacker/sol_refactored/common/config.py +++ b/tacker/sol_refactored/common/config.py @@ -161,6 +161,9 @@ PROMETHEUS_PLUGIN_OPTS = [ 'This configuration is changed in case of replacing ' 'the original function with a vendor specific ' 'function.')), + cfg.BoolOpt('test_rule_with_promtool', + default=False, + help=_('Enable rule file validation using promtool.')), ] CONF.register_opts(PROMETHEUS_PLUGIN_OPTS, 'prometheus_plugin') diff --git a/tacker/sol_refactored/common/prometheus_plugin.py b/tacker/sol_refactored/common/prometheus_plugin.py index 68e6bdfdc..f33093ac9 100644 --- a/tacker/sol_refactored/common/prometheus_plugin.py +++ b/tacker/sol_refactored/common/prometheus_plugin.py @@ -21,6 +21,7 @@ import paramiko import re import tempfile +from keystoneauth1 import exceptions as ks_exc from oslo_log import log as logging from oslo_utils import uuidutils from tacker.sol_refactored.api import prometheus_plugin_validator as validator @@ -37,6 +38,7 @@ from tacker.sol_refactored import objects LOG = logging.getLogger(__name__) +logging.getLogger("paramiko").setLevel(logging.WARNING) CONF = cfg.CONF @@ -510,12 +512,23 @@ class PrometheusPluginPm(PrometheusPlugin, mon_base.MonitoringPlugin): def delete_rules(self, context, pm_job): target_list, reload_list = self.get_access_info(pm_job) - for info in target_list: - self._delete_rule( - info['host'], info['port'], info['user'], - info['password'], info['path'], pm_job.id) + for target in target_list: + try: + self._delete_rule( + target['host'], target['port'], target['user'], + target['password'], target['path'], pm_job.id) + except (sol_ex.PrometheusPluginError, ks_exc.ClientException, + paramiko.SSHException): + # This exception is ignored. DELETE /pm_jobs/{id} + # will be success even if _delete_rule() is failed. + # Because the rule file was already deleted. + pass for uri in reload_list: - self.reload_prom_server(context, uri) + try: + self.reload_prom_server(context, uri) + except (sol_ex.PrometheusPluginError, ks_exc.ClientException, + paramiko.SSHException): + pass def decompose_metrics(self, pm_job): if pm_job.objectType in {'Vnf', 'Vnfc'}: @@ -528,9 +541,10 @@ class PrometheusPluginPm(PrometheusPlugin, mon_base.MonitoringPlugin): def reload_prom_server(self, context, reload_uri): resp, _ = self.client.do_request( reload_uri, "PUT", context=context) - if resp.status_code != 202: - LOG.error("reloading request to prometheus is failed: %d.", - resp.status_code) + if resp.status_code >= 400 and resp.status_code < 600: + raise sol_ex.PrometheusPluginError( + f"Reloading request to prometheus is failed: " + f"{resp.status_code}.") def _upload_rule(self, rule_group, host, port, user, password, path, pm_job_id): @@ -544,6 +558,25 @@ class PrometheusPluginPm(PrometheusPlugin, mon_base.MonitoringPlugin): client.connect(username=user, password=password) sftp = paramiko.SFTPClient.from_transport(client) sftp.put(filename, f'{path}/{pm_job_id}.json') + self.verify_rule(host, port, user, password, path, pm_job_id) + + def verify_rule(self, host, port, user, password, path, pm_job_id): + if not CONF.prometheus_plugin.test_rule_with_promtool: + return + with paramiko.SSHClient() as client: + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + client.connect(host, port=port, username=user, password=password) + command = f"promtool check rules {path}/{pm_job_id}.json" + LOG.info("Rule file validation command: %s", command) + _, stdout, stderr = client.exec_command(command) + if stdout.channel.recv_exit_status() != 0: + error_byte = stderr.read() + error_str = error_byte.decode('utf-8') + LOG.error( + "Rule file validation with promtool failed: %s", + error_str) + raise sol_ex.PrometheusPluginError( + "Rule file validation with promtool failed.") def get_access_info(self, pm_job): target_list = [] @@ -579,12 +612,32 @@ class PrometheusPluginPm(PrometheusPlugin, mon_base.MonitoringPlugin): def upload_rules( self, context, target_list, reload_list, rule_group, pm_job): - for info in target_list: - self._upload_rule( - rule_group, info['host'], info['port'], info['user'], - info['password'], info['path'], pm_job.id) - for uri in reload_list: - self.reload_prom_server(context, uri) + def _cleanup_error(target_list): + for target in target_list: + try: + self._delete_rule(target['host'], target['port'], + target['user'], target['password'], target['path'], + pm_job.id) + except (sol_ex.PrometheusPluginError, ks_exc.ClientException, + paramiko.SSHException): + pass + + try: + for target in target_list: + self._upload_rule( + rule_group, target['host'], target['port'], + target['user'], target['password'], target['path'], + pm_job.id) + for uri in reload_list: + self.reload_prom_server(context, uri) + except (sol_ex.PrometheusPluginError, ks_exc.ClientException, + paramiko.SSHException) as e: + LOG.error("failed to upload rule files: %s", e.args[0]) + _cleanup_error(target_list) + raise e + except Exception as e: + _cleanup_error(target_list) + raise e def get_vnf_instances(self, context, pm_job): object_instance_ids = list(set(pm_job.objectInstanceIds)) diff --git a/tacker/sol_refactored/conductor/prometheus_plugin_driver.py b/tacker/sol_refactored/conductor/prometheus_plugin_driver.py index 2688bf711..7a93480d1 100644 --- a/tacker/sol_refactored/conductor/prometheus_plugin_driver.py +++ b/tacker/sol_refactored/conductor/prometheus_plugin_driver.py @@ -60,4 +60,4 @@ class PrometheusPluginDriver(): url = f'{ep}/vnflcm/v2/vnf_instances/{vnf_instance_id}/scale' resp, _ = self.client.do_request( url, "POST", context=context, body=scale_req, version="2.0.0") - LOG.info("AutoHealing request is processed: %d.", resp.status_code) + LOG.info("AutoScaling request is processed: %d.", resp.status_code) diff --git a/tacker/sol_refactored/controller/vnfpm_v2.py b/tacker/sol_refactored/controller/vnfpm_v2.py index f40d2f796..a25ca38b2 100644 --- a/tacker/sol_refactored/controller/vnfpm_v2.py +++ b/tacker/sol_refactored/controller/vnfpm_v2.py @@ -199,6 +199,7 @@ class VnfPmControllerV2(sol_wsgi.SolAPIController): try: self.plugin.create_job(context=context, pm_job=pm_job) except sol_ex.PrometheusPluginError as e: + LOG.error("Failed to create PM job: %s", e.args[0]) raise sol_ex.PrometheusSettingFailed from e pm_job.create(context) diff --git a/tacker/tests/functional/sol_kubernetes_v2/samples/tacker-monitoring-test.zip b/tacker/tests/functional/sol_kubernetes_v2/samples/tacker-monitoring-test.zip index 94b4370c0e31badddc6615abbf95727c29db4a56..babbf545697e8257e472320490895820700fef19 100644 GIT binary patch literal 6366 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 literal 4873 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 CrK&>!