From 8c13244e25670240762003a5fcbf62a44754c9e5 Mon Sep 17 00:00:00 2001 From: Rodion Promyshlennikov Date: Wed, 4 May 2016 16:06:48 +0300 Subject: [PATCH] Add basic classes and structure for tests Add basic bvt tests for influxdb_grafana. Add helpers modules. Change-Id: I698661a478674b7810f688433000ca39cdf35f3c --- stacklight_tests/base_test.py | 94 +++++++++++++ stacklight_tests/helpers/checkers.py | 24 ++++ stacklight_tests/helpers/helpers.py | 88 +++++++++++++ stacklight_tests/helpers/remote_ops.py | 38 ++++++ stacklight_tests/helpers/ui_tester.py | 18 +++ stacklight_tests/influxdb_grafana/api.py | 109 +++++++++++++++ .../influxdb_grafana/plugin_settings.py | 50 +++++++ .../influxdb_grafana/test_smoke_bvt.py | 124 ++++++++++++++++++ stacklight_tests/settings.py | 8 ++ stacklight_tests/toolchain/base_test.py | 40 ++++++ stacklight_tests/toolchain/plugin_settings.py | 21 +++ tox.ini | 2 +- 12 files changed, 615 insertions(+), 1 deletion(-) create mode 100644 stacklight_tests/base_test.py create mode 100644 stacklight_tests/helpers/checkers.py create mode 100644 stacklight_tests/helpers/helpers.py create mode 100644 stacklight_tests/helpers/remote_ops.py create mode 100644 stacklight_tests/helpers/ui_tester.py create mode 100644 stacklight_tests/influxdb_grafana/api.py create mode 100644 stacklight_tests/influxdb_grafana/plugin_settings.py create mode 100644 stacklight_tests/influxdb_grafana/test_smoke_bvt.py create mode 100644 stacklight_tests/settings.py create mode 100644 stacklight_tests/toolchain/base_test.py create mode 100644 stacklight_tests/toolchain/plugin_settings.py diff --git a/stacklight_tests/base_test.py b/stacklight_tests/base_test.py new file mode 100644 index 0000000..0220d09 --- /dev/null +++ b/stacklight_tests/base_test.py @@ -0,0 +1,94 @@ +# Copyright 2016 Mirantis, Inc. +# +# 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. + +import abc + +from fuelweb_test.tests import base_test_case +import six + +from stacklight_tests.helpers import checkers +from stacklight_tests.helpers import helpers +from stacklight_tests.helpers import remote_ops +from stacklight_tests.helpers import ui_tester +from stacklight_tests import settings + + +@six.add_metaclass(abc.ABCMeta) +class PluginApi(object): + """Common test class to operate with StackLight plugin.""" + + def __init__(self): + self.test = base_test_case.TestBasic() + self.env = self.test.env + self.settings = self.get_plugin_settings() + self.helpers = helpers.PluginHelper(self.env) + self.checkers = checkers + self.remote_ops = remote_ops + self.ui_tester = ui_tester.UITester() + + def __getattr__(self, item): + return getattr(self.test, item) + + @property + def base_nodes(self): + base_nodes = { + 'slave-01': ['controller'], + 'slave-02': ['compute', 'cinder'], + 'slave-03': self.settings.role_name, + } + return base_nodes + + @property + def full_ha_nodes(self): + full_ha_nodes = { + 'slave-01': ['controller'], + 'slave-02': ['controller'], + 'slave-03': ['controller'], + 'slave-04': ['compute', 'cinder'], + 'slave-05': ['compute', 'cinder'], + 'slave-06': ['compute', 'cinder'], + 'slave-07': self.settings.role_name, + 'slave-08': self.settings.role_name, + 'slave-09': self.settings.role_name, + } + return full_ha_nodes + + def create_cluster(self, cluster_settings=None, + mode=settings.DEPLOYMENT_MODE): + return helpers.create_cluster( + self.env, + name=self.__class__.__name__, + cluster_settings=cluster_settings, + mode=mode, + ) + + @abc.abstractmethod + def get_plugin_settings(self): + pass + + @abc.abstractmethod + def prepare_plugin(self): + pass + + @abc.abstractmethod + def activate_plugin(self): + pass + + @abc.abstractmethod + def get_plugin_vip(self): + pass + + @abc.abstractmethod + def check_plugin_online(self): + pass diff --git a/stacklight_tests/helpers/checkers.py b/stacklight_tests/helpers/checkers.py new file mode 100644 index 0000000..6dba36a --- /dev/null +++ b/stacklight_tests/helpers/checkers.py @@ -0,0 +1,24 @@ +# Copyright 2016 Mirantis, Inc. +# +# 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. + +from proboscis import asserts +import requests + + +def check_http_get_response(url, expected_code=200, msg=None, **kwargs): + msg = msg or "%s responded with {0}, expected {1}" % url + r = requests.get(url, **kwargs) + asserts.assert_equal( + r.status_code, expected_code, msg.format(r.status_code, expected_code)) + return r diff --git a/stacklight_tests/helpers/helpers.py b/stacklight_tests/helpers/helpers.py new file mode 100644 index 0000000..f21c51a --- /dev/null +++ b/stacklight_tests/helpers/helpers.py @@ -0,0 +1,88 @@ +# Copyright 2016 Mirantis, Inc. +# +# 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. + +import os +import urllib2 + +from fuelweb_test import logger +from proboscis import asserts + +from stacklight_tests import settings + + +def create_cluster( + env, name, cluster_settings=None, mode=settings.DEPLOYMENT_MODE): + return env.fuel_web.create_cluster( + name=name, settings=cluster_settings, mode=mode) + + +class PluginHelper(object): + """Class for common help functions.""" + + def __init__(self, env): + self.env = env + self.fuel_web = self.env.fuel_web + self._cluster_id = None + + @property + def cluster_id(self): + if self._cluster_id is None: + try: + self._cluster_id = self.fuel_web.get_last_created_cluster() + except urllib2.URLError: + raise EnvironmentError("No cluster was created.") + return self._cluster_id + + @cluster_id.setter + def cluster_id(self, value): + self._cluster_id = value + + def prepare_plugin(self, plugin_path): + """Upload and install plugin by path.""" + self.env.admin_actions.upload_plugin(plugin=plugin_path) + self.env.admin_actions.install_plugin( + plugin_file_name=os.path.basename(plugin_path)) + + def activate_plugin(self, name, version, options): + """Activate and check exist plugin.""" + msg = "Plugin couldn't be enabled. Check plugin version. Test aborted" + asserts.assert_true( + self.fuel_web.check_plugin_exists(self.cluster_id, name), + msg) + self.fuel_web.update_plugin_settings( + self.cluster_id, name, version, options) + + def get_plugin_vip(self, vip_name): + """Get plugin IP.""" + networks = self.fuel_web.client.get_networks(self.cluster_id) + vip = networks.get('vips').get(vip_name, {}).get('ipaddr', None) + asserts.assert_is_not_none( + vip, "Failed to get the IP of {} server".format(vip_name)) + + logger.debug("Check that {} is ready".format(vip_name)) + return vip + + def get_all_ready_nodes(self): + return [node for node in + self.fuel_web.client.list_cluster_nodes(self.cluster_id) + if node["status"] == "ready"] + + def deploy_cluster(self, nodes_roles): + """Method to deploy cluster with provided node roles.""" + self.fuel_web.update_nodes(self.cluster_id, nodes_roles) + self.fuel_web.deploy_cluster_wait(self.cluster_id) + + def run_ostf(self, *args, **kwargs): + kwargs.update({"cluster_id": self.cluster_id}) + self.fuel_web.run_ostf(*args, **kwargs) diff --git a/stacklight_tests/helpers/remote_ops.py b/stacklight_tests/helpers/remote_ops.py new file mode 100644 index 0000000..d4c1e94 --- /dev/null +++ b/stacklight_tests/helpers/remote_ops.py @@ -0,0 +1,38 @@ +# Copyright 2016 Mirantis, Inc. +# +# 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. + + +def get_all_bridged_interfaces_for_node(remote, excluded_criteria=None): + # TODO(rpromyshlennikov): do filtration on python side + excluded_criteria_cmd = ( + " | grep -v '%s'" % excluded_criteria + if excluded_criteria else "") + cmd = "brctl show | awk '/br-/{{print $1}}'{excluded}".format( + excluded=excluded_criteria_cmd) + interfaces = remote.check_call(cmd)["stdout"] + return [iface.strip() for iface in interfaces] + + +def switch_interface(remote, interface, method="up"): + cmd = "if{method} {interface}".format(method=method, + interface=interface) + remote.check_call(cmd) + + +def simulate_network_interrupt_on_node(remote): + cmd = ( + "(/sbin/iptables -I INPUT -j DROP " + "&& sleep 30 " + "&& /sbin/iptables -D INPUT -j DROP) 2>&1>/dev/null &") + remote.execute(cmd) diff --git a/stacklight_tests/helpers/ui_tester.py b/stacklight_tests/helpers/ui_tester.py new file mode 100644 index 0000000..6824487 --- /dev/null +++ b/stacklight_tests/helpers/ui_tester.py @@ -0,0 +1,18 @@ +# Copyright 2016 Mirantis, Inc. +# +# 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. + + +class UITester(object): + # NOTE(rpromyshlennikov): to prepare to move UI test + pass diff --git a/stacklight_tests/influxdb_grafana/api.py b/stacklight_tests/influxdb_grafana/api.py new file mode 100644 index 0000000..e3f9530 --- /dev/null +++ b/stacklight_tests/influxdb_grafana/api.py @@ -0,0 +1,109 @@ +# Copyright 2016 Mirantis, Inc. +# +# 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. + +from devops.helpers import helpers as devops_helpers +from fuelweb_test import logger +from proboscis import asserts +import requests + +from stacklight_tests import base_test +from stacklight_tests.influxdb_grafana import plugin_settings + + +class InfluxdbPluginApi(base_test.PluginApi): + def get_plugin_settings(self): + return plugin_settings + + def prepare_plugin(self): + self.helpers.prepare_plugin(self.settings.plugin_path) + + def activate_plugin(self): + self.helpers.activate_plugin( + self.settings.name, self.settings.version, self.settings.options) + + def get_plugin_vip(self): + return self.helpers.get_plugin_vip(self.settings.vip_name) + + def make_request_to_influx(self, + db=plugin_settings.influxdb_db_name, + user=plugin_settings.influxdb_rootuser, + password=plugin_settings.influxdb_rootpass, + query="", + expected_code=200): + influxdb_vip = self.get_plugin_vip() + + params = { + "db": db, + "u": user, + "p": password, + "q": query, + } + + msg = "InfluxDB responded with {0}, expected {1}" + r = self.checkers.check_http_get_response( + self.settings.influxdb_url.format(influxdb_vip), + expected_code=expected_code, msg=msg, params=params) + return r + + def check_plugin_online(self): + self.make_request_to_influx(query="show measurements") + + logger.debug("Check that the Grafana server is running") + + msg = "Grafana server responded with {0}, expected {1}" + self.checkers.check_http_get_response( + self.settings.grafana_url.format( + self.settings.grafana_user, self.settings.grafana_pass, + self.get_plugin_vip()), + msg=msg + ) + + def check_influxdb_nodes_count(self, nodes_count=1): + response = self.make_request_to_influx( + user=self.settings.influxdb_rootuser, + password=self.settings.influxdb_rootpass, + query="show servers") + + nodes_count_responsed = len( + response.json()["results"][0]["series"][0]["values"]) + + msg = "InfluxDB nodes count expected, received instead: {}".format( + nodes_count_responsed) + asserts.assert_equal(nodes_count, nodes_count_responsed, msg) + + def get_influxdb_master_node(self): + influx_master_node = self.helpers.get_master_node_by_role( + self.settings.role_name) + return influx_master_node + + def wait_for_rotation_influx_master(self, + old_master, timeout=5 * 60): + logger.info('Wait a influxDB master node rotation') + msg = "Failed influxDB master rotation from {0}".format(old_master) + devops_helpers.wait( + lambda: old_master != self.get_influxdb_master_node()['fqdn'], + timeout=timeout, timeout_msg=msg) + + def wait_plugin_online(self, timeout=5 * 60): + def check_availability(): + try: + self.check_plugin_online() + return True + except (AssertionError, requests.ConnectionError): + return False + + logger.info('Wait a plugin become online') + msg = "Plugin has not become online after waiting period" + devops_helpers.wait( + check_availability, timeout=timeout, timeout_msg=msg) diff --git a/stacklight_tests/influxdb_grafana/plugin_settings.py b/stacklight_tests/influxdb_grafana/plugin_settings.py new file mode 100644 index 0000000..e0fe820 --- /dev/null +++ b/stacklight_tests/influxdb_grafana/plugin_settings.py @@ -0,0 +1,50 @@ +# Copyright 2016 Mirantis, Inc. +# +# 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. + +from stacklight_tests import settings + + +name = 'influxdb_grafana' +version = '0.9.0' +role_name = ['influxdb_grafana'] +vip_name = 'influxdb' +plugin_path = settings.INFLUXDB_GRAFANA_PLUGIN_PATH + +influxdb_db_name = "lma" +influxdb_user = 'influxdb' +influxdb_pass = 'influxdbpass' +influxdb_rootuser = 'root' +influxdb_rootpass = 'r00tme' +influxdb_url = "http://{0}:8086/query" + +grafana_user = 'grafana' +grafana_pass = 'grafanapass' +grafana_url = "http://{0}:{1}@{2}:8000/api/org" + +mysql_mode = 'local' +mysql_dbname = 'grafanalma' +mysql_user = 'grafanalma' +mysql_pass = 'mysqlpass' + +options = { + 'influxdb_rootpass/value': influxdb_rootpass, + 'influxdb_username/value': influxdb_user, + 'influxdb_userpass/value': influxdb_pass, + 'grafana_username/value': grafana_user, + 'grafana_userpass/value': grafana_pass, + 'mysql_mode/value': mysql_mode, + 'mysql_dbname/value': mysql_dbname, + 'mysql_username/value': mysql_user, + 'mysql_password/value': mysql_pass, +} diff --git a/stacklight_tests/influxdb_grafana/test_smoke_bvt.py b/stacklight_tests/influxdb_grafana/test_smoke_bvt.py new file mode 100644 index 0000000..e7a6c1b --- /dev/null +++ b/stacklight_tests/influxdb_grafana/test_smoke_bvt.py @@ -0,0 +1,124 @@ +# Copyright 2016 Mirantis, Inc. +# +# 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. + +from fuelweb_test.helpers.decorators import log_snapshot_after_test +from fuelweb_test.tests import base_test_case +from proboscis import test + +from stacklight_tests.influxdb_grafana import api + + +@test(groups=["plugins"]) +class TestInfluxdbPlugin(api.InfluxdbPluginApi): + """Class for smoke testing the InfluxDB-Grafana plugin.""" + + @test(depends_on=[base_test_case.SetupEnvironment.prepare_slaves_3], + groups=["install_influxdb_grafana", "install", + "influxdb_grafana", "smoke"]) + @log_snapshot_after_test + def install_influxdb_grafana_plugin(self): + """Install InfluxDB-Grafana plugin and check it exists + + Scenario: + 1. Upload plugin to the master node + 2. Install plugin + 3. Create cluster + 4. Check that plugin exists + + Duration 20m + """ + self.env.revert_snapshot("ready_with_3_slaves") + + self.prepare_plugin() + + self.create_cluster() + + self.activate_plugin() + + @test(depends_on=[base_test_case.SetupEnvironment.prepare_slaves_3], + groups=["deploy_influxdb_grafana", "deploy", + "influxdb_grafana", "smoke"]) + @log_snapshot_after_test + def deploy_influxdb_grafana_plugin(self): + """Deploy a cluster with the InfluxDB-Grafana plugin + + Scenario: + 1. Upload plugin to the master node + 2. Install plugin + 3. Create cluster + 4. Add 1 node with controller role + 5. Add 1 node with compute and cinder roles + 6. Add 1 node with influxdb_grafana role + 7. Deploy the cluster + 8. Check that plugin is working + 9. Run OSTF + + Duration 60m + Snapshot deploy_influxdb_grafana_plugin + """ + self.check_run("deploy_influxdb_grafana_plugin") + self.env.revert_snapshot("ready_with_3_slaves") + + self.prepare_plugin() + + self.create_cluster() + + self.activate_plugin() + + self.helpers.deploy_cluster(self.base_nodes) + + self.check_plugin_online() + + self.helpers.run_ostf() + + self.env.make_snapshot("deploy_influxdb_grafana_plugin", is_make=True) + + @test(depends_on=[base_test_case.SetupEnvironment.prepare_slaves_9], + groups=["deploy_ha_influxdb_grafana", "deploy", "deploy_ha" + "influxdb_grafana", "smoke"]) + @log_snapshot_after_test + def deploy_ha_influxdb_grafana_plugin(self): + """Deploy a cluster with the InfluxDB-Grafana plugin in HA mode + + Scenario: + 1. Upload plugin to the master node + 2. Install plugin + 3. Create cluster + 4. Add 3 nodes with controller role + 5. Add 3 nodes with compute and cinder roles + 6. Add 3 nodes with influxdb_grafana role + 7. Deploy the cluster + 8. Check that plugin is working + 9. Run OSTF + + Duration 120m + Snapshot deploy_ha_influxdb_grafana_plugin + """ + self.check_run("deploy_ha_influxdb_grafana_plugin") + self.env.revert_snapshot("ready_with_9_slaves") + + self.prepare_plugin() + + self.create_cluster() + + self.activate_plugin() + + self.helpers.deploy_cluster(self.full_ha_nodes) + + self.check_plugin_online() + + self.helpers.run_ostf() + + self.env.make_snapshot("deploy_ha_influxdb_grafana_plugin", + is_make=True) diff --git a/stacklight_tests/settings.py b/stacklight_tests/settings.py new file mode 100644 index 0000000..fac8d0b --- /dev/null +++ b/stacklight_tests/settings.py @@ -0,0 +1,8 @@ +from fuelweb_test.settings import * # noqa + +LMA_COLLECTOR_PLUGIN_PATH = os.environ.get('LMA_COLLECTOR_PLUGIN_PATH') +LMA_INFRA_ALERTING_PLUGIN_PATH = os.environ.get( + 'LMA_INFRA_ALERTING_PLUGIN_PATH') +ELASTICSEARCH_KIBANA_PLUGIN_PATH = os.environ.get( + 'ELASTICSEARCH_KIBANA_PLUGIN_PATH') +INFLUXDB_GRAFANA_PLUGIN_PATH = os.environ.get('INFLUXDB_GRAFANA_PLUGIN_PATH') diff --git a/stacklight_tests/toolchain/base_test.py b/stacklight_tests/toolchain/base_test.py new file mode 100644 index 0000000..970e882 --- /dev/null +++ b/stacklight_tests/toolchain/base_test.py @@ -0,0 +1,40 @@ +# Copyright 2016 Mirantis, Inc. +# +# 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. + +from stacklight_tests import base_test +from stacklight_tests.influxdb_grafana import api as influx_grafana_api + + +class Collector(base_test.PluginApi): + def __init__(self): + super(Collector, self).__init__() + self.plugins = [influx_grafana_api.InfluxdbPluginApi()] + + def get_plugin_settings(self): + pass + + def prepare_plugin(self): + for plugin in self.plugins: + plugin.prepare_plugin() + + def activate_plugin(self): + for plugin in self.plugins: + plugin.activate_plugin() + + def get_plugin_vip(self): + pass + + def check_plugin_online(self): + for plugin in self.plugins: + plugin.check_plugin_online() diff --git a/stacklight_tests/toolchain/plugin_settings.py b/stacklight_tests/toolchain/plugin_settings.py new file mode 100644 index 0000000..e4b0c5b --- /dev/null +++ b/stacklight_tests/toolchain/plugin_settings.py @@ -0,0 +1,21 @@ +# Copyright 2016 Mirantis, Inc. +# +# 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. + +from stacklight_tests.influxdb_grafana import ( + plugin_settings as influxdb_grafana_settings) + +name = 'toolchain' +version = '0.9.0' +# NOTE(rpromyshlennikov): use list(set(plugin1.role_name + plugin2.role_name)) +role_name = influxdb_grafana_settings.role_name diff --git a/tox.ini b/tox.ini index 60142d9..e76d97a 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ distribute = false [flake8] filename=*.py -ignore = H703 +ignore = H405, H703 show-source = true exclude = .venv,.git,.tox,dist,doc,*egg,*lib/python*,build,releasenotes,tmp,utils/fuel-qa-builder/venv* max-complexity=25