Add cache and offload tasks
- use uwsgi cache2 as workers shared storage
- offload case create/close with uwsgi mules
- refactoring
Change-Id: I25d44e87e552d701f7851f28dba3e7ffc218399a
Related-bug: PROD-30321 (PROD:30321)
diff --git a/sf_notifier/__init__.py b/sf_notifier/__init__.py
index e69de29..4e00609 100644
--- a/sf_notifier/__init__.py
+++ b/sf_notifier/__init__.py
@@ -0,0 +1,14 @@
+# Copyright 2018: Mirantis Inc.
+# All Rights Reserved.
+#
+# 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/sf_notifier/helpers.py b/sf_notifier/helpers.py
index e9c221b..d2c713b 100644
--- a/sf_notifier/helpers.py
+++ b/sf_notifier/helpers.py
@@ -1,18 +1,3 @@
-# Copyright 2018: Mirantis Inc.
-# All Rights Reserved.
-#
-# 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
diff --git a/sf_notifier/salesforce/__init__.py b/sf_notifier/salesforce/__init__.py
index e69de29..4e00609 100644
--- a/sf_notifier/salesforce/__init__.py
+++ b/sf_notifier/salesforce/__init__.py
@@ -0,0 +1,14 @@
+# Copyright 2018: Mirantis Inc.
+# All Rights Reserved.
+#
+# 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/sf_notifier/salesforce/cache.py b/sf_notifier/salesforce/cache.py
new file mode 100644
index 0000000..25d76f4
--- /dev/null
+++ b/sf_notifier/salesforce/cache.py
@@ -0,0 +1,63 @@
+import pickle
+
+try:
+ import uwsgi
+except ImportError:
+ def uwsgi_lock(function):
+ return function
+
+ class Uwsgi(dict):
+ def cache_get(self, *args, **kwargs):
+ return self.get(*args, **kwargs)
+
+ def cache_set(self, *args, **kwargs):
+ return self.set(*args, **kwargs)
+
+ def cache_update(self, *args, **kwargs):
+ return self.update(*args, **kwargs)
+
+ def cache_del(self, *args, **kwargs):
+ return self.__delitem__(*args, **kwargs)
+
+ def cache_exists(self, *args, **kwargs):
+ return self.__contains__(*args, **kwargs)
+
+ uwsgi = Uwsgi()
+
+
+class Cache(dict):
+ def get(self, key):
+ return self.__getitem__(key)
+
+ def set(self, key, value):
+ return self.__setitem__(key, value)
+
+ def update(self, key, value):
+ if not self.__contains__(key):
+ return self.set(key, value)
+
+ dump = pickle.dumps(value)
+ return uwsgi.cache_update(key, dump)
+
+ def delete(self, key):
+ if self.__contains__(key):
+ return self.__delitem__(key)
+
+ def __getitem__(self, key):
+ if not self.__contains__(key):
+ return None
+
+ dump = uwsgi.cache_get(key)
+ return pickle.loads(dump)
+
+ def __setitem__(self, key, value):
+ dump = pickle.dumps(value)
+ if self.__contains__(key):
+ uwsgi.cache_update(key, dump)
+ return uwsgi.cache_set(key, dump)
+
+ def __delitem__(self, key):
+ return uwsgi.cache_del(key)
+
+ def __contains__(self, key):
+ return uwsgi.cache_exists(key)
diff --git a/sf_notifier/salesforce/client.py b/sf_notifier/salesforce/client.py
index 71ada18..ee5db91 100644
--- a/sf_notifier/salesforce/client.py
+++ b/sf_notifier/salesforce/client.py
@@ -1,95 +1,26 @@
-# Copyright 2018: Mirantis Inc.
-# All Rights Reserved.
-#
-# 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 fcntl
-import hashlib
import logging
-import os
import time
import uuid
-from contextlib import contextmanager
-
-from cachetools import TTLCache
from prometheus_client import Counter, Gauge
from requests import Session
-from requests.exceptions import ConnectionError as RequestsConnectionError
from simple_salesforce import Salesforce
-from simple_salesforce import exceptions as sf_exceptions
+from simple_salesforce.exceptions import (SalesforceAuthenticationFailed,
+ SalesforceMalformedRequest,
+ SalesforceResourceNotFound)
+from cache import Cache
+from decorators import flocked, sf_auth_retry
+from mixins import SalesforceMixin
+from settings import CASE_STATUS, SESSION_FILE, STATE_MAP
-STATE_MAP = {
- 'OK': '060 Informational',
- 'UP': '060 Informational',
- 'UNKNOWN': '070 Unknown',
- 'WARNING': '080 Warning',
- 'MINOR': '080 Warning',
- 'MAJOR': '090 Critical',
- 'CRITICAL': '090 Critical',
- 'DOWN': '090 Critical',
- 'UNREACHABLE': '090 Critical',
-}
-
-CONFIG_FIELD_MAP = {
- 'auth_url': 'instance_url',
- 'username': 'username',
- 'password': 'password',
- 'organization_id': 'organizationId',
- 'environment_id': 'environment_id',
- 'sandbox_enabled': 'domain',
-}
-
-ALLOWED_HASHING = ('md5', 'sha256')
-SESSION_FILE = '/tmp/session'
logger = logging.getLogger(__name__)
-@contextmanager
-def flocked(fd):
- try:
- fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
- yield
- except IOError:
- logger.info('Session file locked. Waiting 5 seconds...')
- time.sleep(5)
- finally:
- fcntl.flock(fd, fcntl.LOCK_UN)
-
-
-def sf_auth_retry(method):
- def wrapper(self, *args, **kwargs):
- try:
- return method(self, *args, **kwargs)
- except sf_exceptions.SalesforceExpiredSession:
- logger.warning('Salesforce session expired.')
- self.auth()
- except RequestsConnectionError:
- logger.error('Salesforce connection error.')
- self.auth()
- return method(self, *args, **kwargs)
- return wrapper
-
-
-class SfNotifierError(Exception):
- pass
-
-
-class SalesforceClient(object):
+class SalesforceClient(SalesforceMixin):
def __init__(self, config):
self.metrics = {
@@ -100,47 +31,16 @@
self.config = self._validate_config(config)
self.hash_func = self._hash_func()
self.environment = self.config.pop('environment_id')
- self._registered_alerts = TTLCache(maxsize=2048, ttl=300)
+ self._registered_alerts = Cache()
self.sf = None
self.session = Session()
self.auth()
- @staticmethod
- def _hash_func():
- name = os.environ.get('SF_NOTIFIER_ALERT_ID_HASH_FUNC', 'sha256')
- if name in ALLOWED_HASHING:
- return getattr(hashlib, name)
- return hashlib.sha256
-
- @staticmethod
- def _validate_config(config):
- kwargs = {}
-
- for param, field in CONFIG_FIELD_MAP.iteritems():
- setting_var = param.upper()
- env_var = 'SFDC_{}'.format(setting_var)
- kwargs[field] = os.environ.get(
- env_var, config.get(setting_var))
-
- if field == 'domain':
- if kwargs[field] in ['true', 'True', True]:
- kwargs[field] = 'test'
- else:
- del kwargs[field]
- continue
-
- if kwargs[field] is None:
- msg = ('Invalid config: missing "{}" field or "{}" environment'
- ' variable.').format(param, env_var)
- logger.error(msg)
- raise SfNotifierError(msg)
- return kwargs
-
def _auth(self, config):
try:
config.update({'session': self.session})
self.sf = Salesforce(**config)
- except sf_exceptions.SalesforceAuthenticationFailed as ex:
+ except SalesforceAuthenticationFailed as ex:
logger.error('Salesforce authentication failure: {}.'.format(ex))
self.metrics['sf_auth_ok'].set(0)
return False
@@ -149,13 +49,6 @@
self.metrics['sf_auth_ok'].set(1)
return True
- def _load_session(self, session_file):
- lines = session_file.readlines()
-
- if lines == []:
- return
- return lines[0]
-
def _refresh_ready(self, saved_session):
if saved_session is None:
logger.info('Current session is None.')
@@ -213,18 +106,17 @@
while auth_ok is False:
auth_ok = self._acquire_session()
- def _get_alert_id(self, labels):
- alert_id_data = ''
- for key in sorted(labels):
- alert_id_data += labels[key].replace(".", "\\.")
- return self.hash_func(alert_id_data).hexdigest()
-
@sf_auth_retry
def _create_case(self, subject, body, labels, alert_id):
- if alert_id in self._registered_alerts:
+ cached_alert = self._registered_alerts.get(alert_id)
+
+ if cached_alert is not None and cached_alert['id'] != 'error':
logger.warning('Duplicate case for alert: {}.'.format(alert_id))
- return 1, self._registered_alerts[alert_id]['Id']
+ return cached_alert
+
+ case = self._fmt_alert_update('pending', 1)
+ self._registered_alerts.update(alert_id, case)
severity = labels.get('severity', 'unknown').upper()
payload = {
@@ -244,22 +136,26 @@
self.metrics['sf_request_count'].inc()
case = self.sf.Case.create(payload)
logger.info('Created case: {}.'.format(case))
- except sf_exceptions.SalesforceMalformedRequest as ex:
+ except SalesforceMalformedRequest as ex:
msg = ex.content[0]['message']
err_code = ex.content[0]['errorCode']
if err_code == 'DUPLICATE_VALUE':
logger.warning('Duplicate case: {}.'.format(msg))
case_id = msg.split()[-1]
- self._registered_alerts[alert_id] = {'Id': case_id}
- return 1, case_id
+ case = self._fmt_alert_update(case_id, 1)
+ self._registered_alerts.update(alert_id, case)
+ return case
+ case = self._fmt_alert_update('error', 2, error_msg=msg)
+ self._registered_alerts.update(alert_id, case)
logger.error('Cannot create case: {}.'.format(msg))
self.metrics['sf_error_count'].inc()
raise
- self._registered_alerts[alert_id] = {'Id': case['id']}
- return 0, case['id']
+ case = self._fmt_alert_update(case['id'], 0)
+ self._registered_alerts.update(alert_id, case)
+ return case
@sf_auth_retry
def _close_case(self, case_id):
@@ -282,33 +178,36 @@
logger.info('Try to get case by alert ID: {}.'.format(alert_id))
if alert_id in self._registered_alerts:
- return self._registered_alerts[alert_id]
+ return self._registered_alerts.get(alert_id)
try:
return self.sf.Case.get_by_custom_id('Alert_ID__c', alert_id)
- except sf_exceptions.SalesforceResourceNotFound:
- if self._registered_alerts.get(alert_id):
- del self._registered_alerts[alert_id]
+ except SalesforceResourceNotFound:
+ self._registered_alerts.delete(alert_id)
logger.warning('Alert ID: {} not found.'.format(alert_id))
def create_case(self, subject, body, labels):
- alert_id = self._get_alert_id(labels)
+ alert_id = self.get_alert_id(labels)
- error_code, case_id = self._create_case(subject, body,
- labels, alert_id)
+ case = self._create_case(subject, body, labels, alert_id)
- self._create_feed_item(subject, body, case_id)
+ if case['error_code'] == 0 or \
+ (self._feed_update_ready(case['last_update']) and
+ case['id'] not in CASE_STATUS):
+ self._create_feed_item(subject, body, case['id'])
+ case = self._fmt_alert_update(case['id'], case['error_code'])
+ self._registered_alerts.update(alert_id, case)
- response = {'case_id': case_id, 'alert_id': alert_id}
+ response = {'case_id': case['id'], 'alert_id': alert_id}
- if error_code == 1:
+ if case['error_code'] == 1:
response['status'] = 'duplicate'
else:
response['status'] = 'created'
return response
def close_case(self, labels):
- alert_id = self._get_alert_id(labels)
+ alert_id = self.get_alert_id(labels)
case = self._get_case_by_alert_id(alert_id)
response = {'alert_id': alert_id, 'status': 'resolved'}
@@ -316,9 +215,8 @@
if case is None:
return response
- if self._registered_alerts.get(alert_id):
- del self._registered_alerts[alert_id]
+ self._registered_alerts.delete(alert_id)
- response['case_id'] = case['Id']
- response['closed'] = self._close_case(case['Id'])
+ response['case_id'] = case['id']
+ response['closed'] = self._close_case(case['id'])
return response
diff --git a/sf_notifier/salesforce/decorators.py b/sf_notifier/salesforce/decorators.py
new file mode 100644
index 0000000..82d845e
--- /dev/null
+++ b/sf_notifier/salesforce/decorators.py
@@ -0,0 +1,36 @@
+from contextlib import contextmanager
+import fcntl
+import logging
+import time
+
+from requests.exceptions import ConnectionError as RequestsConnectionError
+from simple_salesforce.exceptions import SalesforceExpiredSession
+
+
+logger = logging.getLogger(__name__)
+
+
+def sf_auth_retry(method):
+ def wrapper(self, *args, **kwargs):
+ try:
+ return method(self, *args, **kwargs)
+ except SalesforceExpiredSession:
+ logger.warning('Salesforce session expired.')
+ self.auth()
+ except RequestsConnectionError:
+ logger.error('Salesforce connection error.')
+ self.auth()
+ return method(self, *args, **kwargs)
+ return wrapper
+
+
+@contextmanager
+def flocked(fd):
+ try:
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
+ yield
+ except IOError:
+ logger.info('Session file locked. Waiting 5 seconds...')
+ time.sleep(5)
+ finally:
+ fcntl.flock(fd, fcntl.LOCK_UN)
diff --git a/sf_notifier/salesforce/exceptions.py b/sf_notifier/salesforce/exceptions.py
new file mode 100644
index 0000000..5ccd54e
--- /dev/null
+++ b/sf_notifier/salesforce/exceptions.py
@@ -0,0 +1,2 @@
+class SfNotifierError(Exception):
+ pass
diff --git a/sf_notifier/salesforce/mixins.py b/sf_notifier/salesforce/mixins.py
new file mode 100644
index 0000000..ce857cd
--- /dev/null
+++ b/sf_notifier/salesforce/mixins.py
@@ -0,0 +1,78 @@
+import datetime
+import hashlib
+import logging
+import os
+
+from exceptions import SfNotifierError
+from settings import (ALLOWED_HASHING, CONFIG_FIELD_MAP,
+ FEED_UPDATE_INTERVAL)
+
+
+logger = logging.getLogger(__name__)
+
+
+class SalesforceMixin(object):
+
+ @staticmethod
+ def _hash_func():
+ name = os.environ.get('SF_NOTIFIER_ALERT_ID_HASH_FUNC', 'sha256')
+ if name in ALLOWED_HASHING:
+ return getattr(hashlib, name)
+ return hashlib.sha256
+
+ @staticmethod
+ def _feed_update_ready(update_time):
+ now = datetime.datetime.now()
+ feed_update_interval = datetime.timedelta(hours=FEED_UPDATE_INTERVAL)
+ return now - update_time >= feed_update_interval
+
+ @staticmethod
+ def _validate_config(config):
+ kwargs = {}
+
+ for param, field in CONFIG_FIELD_MAP.iteritems():
+ setting_var = param.upper()
+ env_var = 'SFDC_{}'.format(setting_var)
+ kwargs[field] = os.environ.get(
+ env_var, config.get(setting_var))
+
+ if field == 'domain':
+ if kwargs[field] in ['true', 'True', True]:
+ kwargs[field] = 'test'
+ else:
+ del kwargs[field]
+ continue
+
+ if kwargs[field] is None:
+ msg = ('Invalid config: missing "{}" field or "{}" environment'
+ ' variable.').format(param, env_var)
+ logger.error(msg)
+ raise SfNotifierError(msg)
+ return kwargs
+
+ @staticmethod
+ def _load_session(session_file):
+ lines = session_file.readlines()
+
+ if lines == []:
+ return
+ return lines[0]
+
+ @staticmethod
+ def _fmt_alert_update(case_id, error_code, error_msg=None, now=None):
+ now = now or datetime.datetime.now()
+ row = {
+ 'id': case_id,
+ 'error_code': error_code,
+ 'last_update': now
+ }
+ if error_msg is not None:
+ row.update({'error': error_msg})
+ return row
+
+ def get_alert_id(self, labels, hash_func=None):
+ hash_func = hash_func or self.hash_func
+ alert_id_data = ''
+ for key in sorted(labels):
+ alert_id_data += labels[key].replace(".", "\\.")
+ return hash_func(alert_id_data).hexdigest()
diff --git a/sf_notifier/salesforce/settings.py b/sf_notifier/salesforce/settings.py
new file mode 100644
index 0000000..a4f3dd3
--- /dev/null
+++ b/sf_notifier/salesforce/settings.py
@@ -0,0 +1,27 @@
+STATE_MAP = {
+ 'OK': '060 Informational',
+ 'UP': '060 Informational',
+ 'UNKNOWN': '070 Unknown',
+ 'WARNING': '080 Warning',
+ 'MINOR': '080 Warning',
+ 'MAJOR': '090 Critical',
+ 'CRITICAL': '090 Critical',
+ 'DOWN': '090 Critical',
+ 'UNREACHABLE': '090 Critical',
+}
+
+CONFIG_FIELD_MAP = {
+ 'auth_url': 'instance_url',
+ 'username': 'username',
+ 'password': 'password',
+ 'organization_id': 'organizationId',
+ 'environment_id': 'environment_id',
+ 'sandbox_enabled': 'domain',
+}
+
+ALLOWED_HASHING = ('md5', 'sha256')
+SESSION_FILE = '/tmp/session'
+# by default allow to send 2 feed items per hour
+FEED_ITEMS_THROTTLING = 2
+FEED_UPDATE_INTERVAL = 1.0 / FEED_ITEMS_THROTTLING
+CASE_STATUS = ('pending', 'error')
diff --git a/sf_notifier/server.py b/sf_notifier/server.py
index 4d3ddc0..9a5da5d 100644
--- a/sf_notifier/server.py
+++ b/sf_notifier/server.py
@@ -1,35 +1,19 @@
-# Copyright 2018: Mirantis Inc.
-# All Rights Reserved.
-#
-# 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 json
from logging.config import dictConfig
+import time
from flask import Flask, Response, jsonify, request
-
from prometheus_client import make_wsgi_app
-
from requests.exceptions import ConnectionError as RequestsConnectionError
+from simple_salesforce.exceptions import SalesforceError
+from simple_settings import settings
+from werkzeug.wsgi import DispatcherMiddleware
+from uwsgidecorators import mulefunc
+
from sf_notifier.helpers import alert_fields_and_action, create_file
from sf_notifier.salesforce.client import SESSION_FILE, SalesforceClient
-
-from simple_salesforce.exceptions import SalesforceError
-
-from simple_settings import settings
-
-from werkzeug.wsgi import DispatcherMiddleware
+from sf_notifier.salesforce.settings import CASE_STATUS
dictConfig(settings.LOGGING)
@@ -52,6 +36,56 @@
})
+@mulefunc
+def offload(action, fields):
+ try:
+ getattr(sf_cli, action)(*fields)
+ except (SalesforceError, RequestsConnectionError) as err:
+ msg = 'Salesforce request failure: {}.'.format(err)
+ sf_cli.metrics['sf_error_count'].inc()
+ app.logger.error(msg)
+
+
+def create_case_results(alert_ids):
+ cases = {}
+ is_error = False
+
+ while len(alert_ids) > len(cases):
+ for alert_id in alert_ids:
+ case = sf_cli._registered_alerts.get(alert_id)
+
+ if case is None:
+ continue
+
+ if case['id'] not in CASE_STATUS:
+ cases[alert_id] = {}
+ cases[alert_id]['case'] = case['id']
+ if case['id'] == 'error':
+ if sf_cli._feed_update_ready(case['last_update']):
+ continue
+ is_error = True
+ cases[alert_id] = {}
+ cases[alert_id]['error'] = case['error']
+
+ time.sleep(0.2)
+ return cases, is_error
+
+
+def close_case_results(alert_ids):
+ # timeout is implicit error
+ cases = {}
+
+ while len(alert_ids) > 0:
+ for alert_id in alert_ids:
+ case = sf_cli._registered_alerts.get(alert_id)
+
+ if case is None:
+ cases[alert_id] = {'status': 'closed'}
+ alert_ids.pop(alert_ids.index(alert_id))
+ time.sleep(0.2)
+ return cases, False
+
+
@app.route('/hook', methods=['POST'])
def webhook_receiver():
@@ -66,31 +100,21 @@
app.logger.info('Received requests: {}'.format(data))
- cases = []
+ alert_ids = []
for alert in data['alerts']:
- try:
- alert['labels']['env_id'] = sf_cli.environment
- fields, action = alert_fields_and_action(alert)
- except KeyError as key:
- msg = 'Alert misses {} key.'.format(key)
- app.logger.error(msg)
- return Response(json.dumps({'error': msg}),
- status=400,
- mimetype='application/json')
+ alert['labels']['env_id'] = sf_cli.environment
+ alert_ids.append(sf_cli.get_alert_id(alert['labels']))
+ fields, action = alert_fields_and_action(alert)
- if fields:
- try:
- cases.append(getattr(sf_cli, action)(*fields))
- except (SalesforceError, RequestsConnectionError) as err:
- msg = 'Salesforce request failure: {}.'.format(err)
- sf_cli.metrics['sf_error_count'].inc()
- app.logger.error(msg)
- return Response(json.dumps({'error': msg}),
- status=500,
- mimetype='application/json')
+ offload(action, fields)
- if len(cases) == 1:
- return jsonify(cases[0])
+ cases, is_error = globals()[action + '_results'](alert_ids)
+
+ if is_error:
+ return Response(json.dumps(cases),
+ status=500,
+ mimetype='application/json')
+
return jsonify(cases)
diff --git a/sf_notifier/tests/test_client.py b/sf_notifier/tests/test_client.py
index 059ea24..f505e68 100644
--- a/sf_notifier/tests/test_client.py
+++ b/sf_notifier/tests/test_client.py
@@ -2,7 +2,8 @@
import pytest
-from sf_notifier.salesforce.client import SalesforceClient, SfNotifierError
+from sf_notifier.salesforce.client import SalesforceClient
+from sf_notifier.salesforce.exceptions import SfNotifierError
ENV_VARS = [
diff --git a/tox.ini b/tox.ini
index 3bbb061..a254a98 100644
--- a/tox.ini
+++ b/tox.ini
@@ -18,16 +18,16 @@
[testenv:flake8]
basepython = python2
commands =
- flake8 -v
+ flake8 -v --ignore I100,I201,W504
[testenv:pycodestyle]
basepython = python2
commands =
- pycodestyle -v
+ pycodestyle -v --ignore I100,W504
[testenv:bandit]
basepython = python2
-commands = bandit -r -x tests -s B108,B110,B410 sf_notifier
+commands = bandit -r -x tests -s B108,B110,B101,B301,B403,B410 sf_notifier
[testenv:venv]
basepython = python2