diff --git a/.ansible-lint b/.ansible-lint index a1152f8..813f365 100644 --- a/.ansible-lint +++ b/.ansible-lint @@ -2,8 +2,4 @@ parseable: true skip_list: # Add skips here only as last resort, like: - # https://github.com/ansible/ansible-lint/issues/557 - - 302 # [E302] mkdir used in place of argument state=directory to file module - - 303 # [E303] ... used in place of ... module - - 208 # [E208] - - 106 # [E106] + - role-name diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c2016e1..41cccfb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,16 @@ --- repos: + - repo: https://github.com/PyCQA/isort + rev: 5.7.0 + hooks: + - id: isort + - repo: https://github.com/python/black.git + rev: 20.8b1 + hooks: + - id: black + language_version: python3 - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.4.0 + rev: v3.4.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace @@ -10,18 +19,25 @@ repos: - id: check-executables-have-shebangs - id: check-merge-conflict - id: debug-statements - - id: flake8 - id: check-yaml files: .*\.(yaml|yml)$ + - repo: https://gitlab.com/pycqa/flake8.git + rev: 3.8.4 + hooks: + - id: flake8 + additional_dependencies: + - flake8-absolute-import + - flake8-black>=0.1.1 + language_version: python3 - repo: https://github.com/adrienverge/yamllint.git - rev: v1.18.0 + rev: v1.26.0 hooks: - id: yamllint files: \.(yaml|yml)$ types: [file, yaml] entry: yamllint --strict -f parsable - repo: https://github.com/ansible/ansible-lint.git - rev: 9da220ae3a11c10c10ee43284ad6cad6d8ba52b7 + rev: v5.0.0 hooks: - id: ansible-lint always_run: true @@ -31,7 +47,7 @@ repos: verbose: true entry: env ANSIBLE_LIBRARY=./library ansible-lint --force-color -v . - repo: https://github.com/openstack-dev/bashate.git - rev: 0.6.0 + rev: 2.0.0 hooks: - id: bashate entry: bashate --error . --verbose --ignore=E006,E040 diff --git a/docs/source/conf.py b/docs/source/conf.py index a25832e..ae65097 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -28,7 +28,8 @@ # # All configuration values have a default; values that are commented out # serve to show the default. -from __future__ import (absolute_import, division, print_function) +from __future__ import absolute_import, division, print_function + import sphinx_rtd_theme __metaclass__ = type @@ -51,31 +52,31 @@ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] html_theme_options = {} # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'TripleO' -copyright = u'2016, RDO CI Team' -bug_tracker = u'Bugzilla' -bug_tracker_url = u'https://bugzilla.redhat.com' +project = u"TripleO" +copyright = u"2016, RDO CI Team" +bug_tracker = u"Bugzilla" +bug_tracker_url = u"https://bugzilla.redhat.com" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '3.0.0' +version = "3.0.0" # The full version, including alpha/beta/rc tags. -release = '3.0.0' +release = "3.0.0" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -106,7 +107,7 @@ exclude_patterns = [] # show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] @@ -114,12 +115,12 @@ pygments_style = 'sphinx' # -- Options for HTML output --------------------------------------------------- -html_static_path = ['_custom'] -html_style = 'custom.css' -html_last_updated_fmt = '%b %d, %Y' +html_static_path = ["_custom"] +html_style = "custom.css" +html_last_updated_fmt = "%b %d, %Y" # Output file base name for HTML help builder. -htmlhelp_basename = 'tripleo-documentor' +htmlhelp_basename = "tripleo-documentor" html_show_sourcelink = True html_show_sphinx = True @@ -130,10 +131,8 @@ html_show_copyright = True latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # 'preamble': '', } @@ -142,4 +141,8 @@ rst_prolog = """ .. |project| replace:: %s .. |bug_tracker| replace:: %s .. |bug_tracker_url| replace:: %s -""" % (project, bug_tracker, bug_tracker_url) +""" % ( + project, + bug_tracker, + bug_tracker_url, +) diff --git a/plugins/module_utils/sova_lib.py b/plugins/module_utils/sova_lib.py index 6e8e1c2..4875874 100644 --- a/plugins/module_utils/sova_lib.py +++ b/plugins/module_utils/sova_lib.py @@ -11,7 +11,7 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import (absolute_import, division, print_function) +from __future__ import absolute_import, division, print_function import gzip import logging @@ -24,9 +24,12 @@ except ImportError: __metaclass__ = type logging.basicConfig( - format=('%(asctime)s - %(name)s - %(levelname)s - ' - '%(module)s.%(funcName)s:%(lineno)d - %(message)s')) -log = logging.getLogger('parser') + format=( + "%(asctime)s - %(name)s - %(levelname)s - " + "%(module)s.%(funcName)s:%(lineno)d - %(message)s" + ) +) +log = logging.getLogger("parser") log.setLevel(logging.ERROR) @@ -48,22 +51,23 @@ class Pattern(object): def setup_regexes(self): self.regexes = {} if self.config: - for regexp in self.config.get('regexes', []): + for regexp in self.config.get("regexes", []): flags = [] - if regexp.get('multiline'): + if regexp.get("multiline"): flags.append(regex_module.MULTILINE) - self.regexes[regexp.get('name')] = regex_module.compile( - r'{0}'.format(regexp.get('regex')), *flags) + self.regexes[regexp.get("name")] = regex_module.compile( + r"{0}".format(regexp.get("regex")), *flags + ) def setup_patterns(self): - self._patterns = self.config.get('patterns', {}) + self._patterns = self.config.get("patterns", {}) if self._patterns: for key in self._patterns: for p in self._patterns[key]: - if p['pattern'] in self.regexes: - p['pattern'] = self.regexes[p['pattern']] - if p['logstash'] in self.regexes: - p['logstash'] = self.regexes[p['logstash']] + if p["pattern"] in self.regexes: + p["pattern"] = self.regexes[p["pattern"]] + if p["logstash"] in self.regexes: + p["logstash"] = self.regexes[p["logstash"]] @property def patterns(self): @@ -94,12 +98,9 @@ def parse(text_file, patterns): with open_func(text_file, "rt") as finput: text = finput.read() for p in patterns: - line_matched = line_match( - p["pattern"], text, exclude=p.get("exclude")) + line_matched = line_match(p["pattern"], text, exclude=p.get("exclude")) if line_matched: - log.debug( - "Found pattern %s in file %s", - repr(p), text_file) + log.debug("Found pattern %s in file %s", repr(p), text_file) ids.append(p["id"]) msgs.append(p["msg"].format(line_matched)) return list(set(ids)), list(set(msgs)) diff --git a/plugins/modules/ara_graphite.py b/plugins/modules/ara_graphite.py index 8fb7f6b..888167a 100644 --- a/plugins/modules/ara_graphite.py +++ b/plugins/modules/ara_graphite.py @@ -11,10 +11,10 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import (absolute_import, division, print_function) +from __future__ import absolute_import, division, print_function __metaclass__ = type -DOCUMENTATION = ''' +DOCUMENTATION = """ --- module: ara_graphite version_added: "1.0.0" @@ -55,9 +55,9 @@ options: required: False default: True type: bool -''' +""" -EXAMPLES = ''' +EXAMPLES = """ - name: Get ARA json data shell: "{{ local_working_dir }}/bin/ara task list --all -f json" register: ara_data @@ -67,7 +67,7 @@ EXAMPLES = ''' ara_data: "{{ ara_task_output.stdout }}" ara_mapping: - "Name of task that deploys overcloud": overcloud.deploy.seconds -''' +""" import ast # noqa: E402 import datetime # noqa: E402 @@ -75,27 +75,28 @@ import socket # noqa: E402 def stamp(x): - '''Convert ISO timestamp to Unix timestamp + """Convert ISO timestamp to Unix timestamp :param x: string with timestamp :return: string with Unix timestamp - ''' - return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S").strftime('%s') + """ + return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S").strftime("%s") def task_length(x): - '''Calculate task length in seconds from "%H:%M:%S" format + """Calculate task length in seconds from "%H:%M:%S" format :param x: datetime string :return: number of seconds spent for task - ''' + """ t = datetime.datetime.strptime(x, "%H:%M:%S") - return datetime.timedelta(hours=t.hour, minutes=t.minute, - seconds=t.second).total_seconds() + return datetime.timedelta( + hours=t.hour, minutes=t.minute, seconds=t.second + ).total_seconds() def translate(mapping, json_data, only_ok): - '''Create data to send to Graphite server in format: + """Create data to send to Graphite server in format: GraphitePath Timestamp TaskDuration GraphitePath is taken from mapping dictionary according to task name. @@ -103,26 +104,27 @@ def translate(mapping, json_data, only_ok): :param mapping: dictionary of mapping task names to graphite paths :param json_data: JSON data with tasks and times :return: list of graphite data - ''' + """ items = [] data = ast.literal_eval(json_data) for task in data: - if not only_ok or (only_ok and task['Status'] in ['changed', 'ok']): - if task['Name'] in mapping: - timestamp, duration = stamp(task['Time Start']), task_length( - task['Duration']) - items.append([mapping[task['Name']], duration, timestamp]) + if not only_ok or (only_ok and task["Status"] in ["changed", "ok"]): + if task["Name"] in mapping: + timestamp, duration = stamp(task["Time Start"]), task_length( + task["Duration"] + ) + items.append([mapping[task["Name"]], duration, timestamp]) return items def send(data, gr_host, gr_port, prefix): - '''Actual sending of data to Graphite server via network + """Actual sending of data to Graphite server via network :param data: list of items to send to Graphite :param gr_host: Graphite host (with optional port) :param prefix: prefix to append before Graphite path :return: True if sent successfully, otherwise False - ''' + """ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(3.0) try: @@ -132,31 +134,31 @@ def send(data, gr_host, gr_port, prefix): for content in data: s.send(prefix + " ".join([str(i) for i in content]) + "\n") s.close() - return True, '' + return True, "" def send_stats(gr_host, gr_port, mapping, json_data, prefix, only_ok): - '''Send ARA statistics to Graphite server + """Send ARA statistics to Graphite server :param gr_host: Graphite host (with optional port) :param mapping: dictionary of mapping task names to graphite paths :param json_data: JSON data with tasks and times :param prefix: prefix to append before Graphite path :return: JSON ansible result - ''' + """ data2send = translate(mapping, json_data, only_ok) response, reason = send(data2send, gr_host, gr_port, prefix) if not response: return { - 'changed': False, - 'failed': True, - 'graphite_host': gr_host, - 'msg': "Can't connect to Graphite: %s" % reason + "changed": False, + "failed": True, + "graphite_host": gr_host, + "msg": "Can't connect to Graphite: %s" % reason, } return { - 'changed': True, - 'graphite_host': gr_host, - 'sent_data': data2send, + "changed": True, + "graphite_host": gr_host, + "sent_data": data2send, } @@ -165,21 +167,22 @@ def main(): module = AnsibleModule( argument_spec=dict( - graphite_host=dict(required=True, type='str'), - graphite_port=dict(required=False, type='int', default=2003), - ara_mapping=dict(required=True, type='dict'), - ara_data=dict(required=True, type='str'), - graphite_prefix=dict(required=False, type='str', default=''), - only_successful_tasks=dict(required=False, type='bool', - default=True) + graphite_host=dict(required=True, type="str"), + graphite_port=dict(required=False, type="int", default=2003), + ara_mapping=dict(required=True, type="dict"), + ara_data=dict(required=True, type="str"), + graphite_prefix=dict(required=False, type="str", default=""), + only_successful_tasks=dict(required=False, type="bool", default=True), ) ) - result = send_stats(module.params['graphite_host'], - module.params['graphite_port'], - module.params['ara_mapping'], - module.params['ara_data'], - module.params['graphite_prefix'], - module.params['only_successful_tasks']) + result = send_stats( + module.params["graphite_host"], + module.params["graphite_port"], + module.params["ara_mapping"], + module.params["ara_data"], + module.params["graphite_prefix"], + module.params["only_successful_tasks"], + ) module.exit_json(**result) diff --git a/plugins/modules/ara_influxdb.py b/plugins/modules/ara_influxdb.py index ef1cd3c..178cbbe 100644 --- a/plugins/modules/ara_influxdb.py +++ b/plugins/modules/ara_influxdb.py @@ -11,10 +11,10 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import (absolute_import, division, print_function) +from __future__ import absolute_import, division, print_function __metaclass__ = type -DOCUMENTATION = ''' +DOCUMENTATION = """ --- module: ara_influxdb version_added: "1.0.0" @@ -90,9 +90,9 @@ options: by default 0. required: False type: int -''' +""" -EXAMPLES = ''' +EXAMPLES = """ - name: Get ARA json data shell: "{{ local_working_dir }}/bin/ara result list --all -f json" register: ara_data @@ -112,7 +112,7 @@ EXAMPLES = ''' standard_fields: false longest_tasks: 15 when: ara_data.stdout != "[]" -''' +""" import ast # noqa pylint: disable=C0413 import datetime # noqa pylint: disable=C0413 @@ -120,98 +120,98 @@ import json # noqa pylint: disable=C0413 import os # noqa pylint: disable=C0413 import re # noqa pylint: disable=C0413 - -SCHEME = '{measure},{tags} {fields} {timestamp}' +SCHEME = "{measure},{tags} {fields} {timestamp}" CUSTOM_MAP = { - 'undercloud_install': ["undercloud-deploy : Install the undercloud"], - 'prepare_images': [ - "overcloud-prep-images : Prepare the overcloud images for deploy"], - 'images_update': [ + "undercloud_install": ["undercloud-deploy : Install the undercloud"], + "prepare_images": [ + "overcloud-prep-images : Prepare the overcloud images for deploy" + ], + "images_update": [ "modify-image : Convert image", "modify-image : Run script on image", - "modify-image : Close qcow2 image" + "modify-image : Close qcow2 image", ], - 'images_build': ["build-images : run the image build script (direct)"], - 'containers_prepare': [ - "overcloud-prep-containers : " - "Prepare for the containerized deployment"], - 'overcloud_deploy': ["overcloud-deploy : Deploy the overcloud"], - 'pingtest': ["validate-simple : Validate the overcloud"], - 'tempest_run': ["validate-tempest : Execute tempest"], - 'undercloud_reinstall': [ - "validate-undercloud : Reinstall the undercloud to check idempotency"], - 'overcloud_delete': [ - "overcloud-delete : check for delete command to complete or fail"], - 'overcloud_upgrade': ["overcloud-upgrade : Upgrade the overcloud", - "tripleo-upgrade : run docker upgrade converge step", - "tripleo-upgrade : run docker upgrade composable " - "step"], - 'undercloud_upgrade': ["tripleo-upgrade : upgrade undercloud"], + "images_build": ["build-images : run the image build script (direct)"], + "containers_prepare": [ + "overcloud-prep-containers : Prepare for the containerized deployment" + ], + "overcloud_deploy": ["overcloud-deploy : Deploy the overcloud"], + "pingtest": ["validate-simple : Validate the overcloud"], + "tempest_run": ["validate-tempest : Execute tempest"], + "undercloud_reinstall": [ + "validate-undercloud : Reinstall the undercloud to check idempotency" + ], + "overcloud_delete": [ + "overcloud-delete : check for delete command to complete or fail" + ], + "overcloud_upgrade": [ + "overcloud-upgrade : Upgrade the overcloud", + "tripleo-upgrade : run docker upgrade converge step", + "tripleo-upgrade : run docker upgrade composable step", + ], + "undercloud_upgrade": ["tripleo-upgrade : upgrade undercloud"], } class InfluxStandardTags(object): - '''InfluxStandardTags contains: + """InfluxStandardTags contains: - calculation of standard job describing parameters as: - * release - * nodepool provider cloud - * zuul pipeline name - * toci_jobtype - and rendering them in tags template + calculation of standard job describing parameters as: + * release + * nodepool provider cloud + * zuul pipeline name + * toci_jobtype + and rendering them in tags template - ''' + """ def branch(self): - return os.environ.get('STABLE_RELEASE') or 'master' + return os.environ.get("STABLE_RELEASE") or "master" def cloud(self): - return os.environ.get('NODEPOOL_PROVIDER', 'null') + return os.environ.get("NODEPOOL_PROVIDER", "null") def pipeline(self): - if os.environ.get('ZUUL_PIPELINE'): - if 'check' in os.environ['ZUUL_PIPELINE']: - return 'check' - elif 'gate' in os.environ['ZUUL_PIPELINE']: - return 'gate' - elif 'periodic' in os.environ['ZUUL_PIPELINE']: - return 'periodic' - return 'null' + if os.environ.get("ZUUL_PIPELINE"): + if "check" in os.environ["ZUUL_PIPELINE"]: + return "check" + elif "gate" in os.environ["ZUUL_PIPELINE"]: + return "gate" + elif "periodic" in os.environ["ZUUL_PIPELINE"]: + return "periodic" + return "null" def toci_jobtype(self): - return os.environ.get('TOCI_JOBTYPE', 'null') + return os.environ.get("TOCI_JOBTYPE", "null") def render(self): - return ('branch=%s,' - 'cloud=%s,' - 'pipeline=%s,' - 'toci_jobtype=%s') % ( - self.branch(), - self.cloud(), - self.pipeline(), - self.toci_jobtype(), + return ("branch=%s," "cloud=%s," "pipeline=%s," "toci_jobtype=%s") % ( + self.branch(), + self.cloud(), + self.pipeline(), + self.toci_jobtype(), ) class InfluxStandardFields(object): - '''InfluxStandardFields contains: + """InfluxStandardFields contains: - calculation of time of job steps as: - * whole job duration - * testing environment preparement - * quickstart files and environment preparement - * zuul host preparement - and rendering them in template + calculation of time of job steps as: + * whole job duration + * testing environment preparement + * quickstart files and environment preparement + * zuul host preparement + and rendering them in template - ''' + """ def job_duration(self): - if os.environ.get('START_JOB_TIME'): - return int( - datetime.datetime.utcnow().strftime("%s")) - int( - os.environ.get('START_JOB_TIME')) + if os.environ.get("START_JOB_TIME"): + return int(datetime.datetime.utcnow().strftime("%s")) - int( + os.environ.get("START_JOB_TIME") + ) return 0 def logs_size(self): @@ -222,51 +222,55 @@ class InfluxStandardFields(object): return datetime.datetime.utcnow().strftime("%s") def testenv_prepare(self): - return os.environ.get('STATS_TESTENV', 0) + return os.environ.get("STATS_TESTENV", 0) def quickstart_prepare(self): - return os.environ.get('STATS_OOOQ', 0) + return os.environ.get("STATS_OOOQ", 0) def zuul_host_prepare(self): - if (os.environ.get('DEVSTACK_GATE_TIMEOUT') and # noqa: W504 - os.environ.get('REMAINING_TIME')): - return (int( - os.environ['DEVSTACK_GATE_TIMEOUT']) - int( - os.environ['REMAINING_TIME'])) * 60 + if os.environ.get("DEVSTACK_GATE_TIMEOUT") and os.environ.get( # noqa: W504 + "REMAINING_TIME" + ): + return ( + int(os.environ["DEVSTACK_GATE_TIMEOUT"]) + - int(os.environ["REMAINING_TIME"]) + ) * 60 return 0 def render(self): - return ('job_duration=%d,' - 'logs_size=%d,' - 'testenv_prepare=%s,' - 'quickstart_prepare=%s,' - 'zuul_host_prepare=%d,' - ) % ( - self.job_duration(), - self.logs_size(), - self.testenv_prepare(), - self.quickstart_prepare(), - self.zuul_host_prepare() + return ( + "job_duration=%d," + "logs_size=%d," + "testenv_prepare=%s," + "quickstart_prepare=%s," + "zuul_host_prepare=%d," + ) % ( + self.job_duration(), + self.logs_size(), + self.testenv_prepare(), + self.quickstart_prepare(), + self.zuul_host_prepare(), ) class InfluxConfiguredFields(object): - '''InfluxConfiguredFields contains calculation: + """InfluxConfiguredFields contains calculation: + + * whole job duration + * testing environment preparement + * quickstart files and environment preparement + * zuul host preparement + and rendering them in template + """ - * whole job duration - * testing environment preparement - * quickstart files and environment preparement - * zuul host preparement - and rendering them in template - ''' def __init__(self, match_map, json_data, only_ok=True): """Set up data for configured field - :param match_map {dict} -- Map of tasks from ansible playbook to - names of data fields in influxDB. - :param json_data: {dict} -- JSON data generated by ARA - :param only_ok=True: {bool} -- to count only passed tasks + :param match_map {dict} -- Map of tasks from ansible playbook to + names of data fields in influxDB. + :param json_data: {dict} -- JSON data generated by ARA + :param only_ok=True: {bool} -- to count only passed tasks """ self.map = match_map self.only_ok = only_ok @@ -281,26 +285,26 @@ class InfluxConfiguredFields(object): def render(self): tasks = self.task_maps() - result = '' + result = "" for task, timest in tasks.items(): result += "%s=%d," % (task, timest) return result class InfluxLongestFields(object): - '''InfluxLongestFields runs calculation of: + """InfluxLongestFields runs calculation of: - tasks that took the longest time. - The tasks could be from undercloud or overcloud playbooks. + tasks that took the longest time. + The tasks could be from undercloud or overcloud playbooks. - ''' + """ def __init__(self, json_data, only_ok=True, top=15): """Constructor for InfluxLongestFields - :param json_data: {dict} -- JSON data generated by ARA - :param only_ok=True: {bool} -- to count only passed tasks - :param top=15: {int} -- how many tasks to send to DB + :param json_data: {dict} -- JSON data generated by ARA + :param only_ok=True: {bool} -- to count only passed tasks + :param top=15: {int} -- how many tasks to send to DB """ self.top = top self.only_ok = only_ok @@ -309,40 +313,37 @@ class InfluxLongestFields(object): def collect_tasks(self): tasks_dict = tasks_times_dict(self.data, self.only_ok) return sorted( - [[k, v] for k, v in tasks_dict.items()], - key=lambda x: x[1], - reverse=True - )[:self.top] + [[k, v] for k, v in tasks_dict.items()], key=lambda x: x[1], reverse=True + )[: self.top] def translate_names(self, names): for i in names: i[0] = re.sub( - r'[^0-9A-z\-_]+', - '', - i[0].replace(":", "__").replace(" ", "_")) + r"[^0-9A-z\-_]+", "", i[0].replace(":", "__").replace(" ", "_") + ) i[1] = int(i[1]) return names def render(self): - result = '' + result = "" for i in self.translate_names(self.collect_tasks()): result += "{0}={1},".format(*i) return result class SovaFields(object): - '''SovaFields provides Sova calculated failure reasons.''' + """SovaFields provides Sova calculated failure reasons.""" def __init__(self, sova_file): """Constructor for SovaFields - :param sova_file: {str} -- path to 'failures_file' of Sova + :param sova_file: {str} -- path to 'failures_file' of Sova """ self.sova_file = sova_file def parse_sova_file(self): if not os.path.exists(self.sova_file): - return '' + return "" with open(self.sova_file) as f: text = f.readlines() reason = text[0] @@ -353,19 +354,19 @@ class SovaFields(object): scheme = 'sova_reason="%s",sova_tag="%s",' res = self.parse_sova_file() if not res: - return scheme % ('', '') + return scheme % ("", "") return scheme % (res[0], res[1]) def tasks_times_dict(tasks, only_ok=True): times_dict = {} for task in tasks: - if not only_ok or task['Status'] in ['changed', 'ok']: - name = task['Name'] + if not only_ok or task["Status"] in ["changed", "ok"]: + name = task["Name"] if name in times_dict: - times_dict[name].append(task['Duration']) + times_dict[name].append(task["Duration"]) else: - times_dict[name] = [task['Duration']] + times_dict[name] = [task["Duration"]] # because of some tasks are executed multiple times we need to count # all of them and make summary of all durations for i in times_dict: @@ -374,26 +375,31 @@ def tasks_times_dict(tasks, only_ok=True): def task_length(x): - '''Calculate task length in seconds from "%H:%M:%S" format + """Calculate task length in seconds from "%H:%M:%S" format Arguments: x {string} -- a timestamp Returns: int -- total seconds for the task - ''' + """ t = datetime.datetime.strptime(x, "%H:%M:%S") - return datetime.timedelta(hours=t.hour, minutes=t.minute, - seconds=t.second).total_seconds() + return datetime.timedelta( + hours=t.hour, minutes=t.minute, seconds=t.second + ).total_seconds() -def translate(measure, json_data, only_ok, - mapped_fields=True, - standard_fields=True, - longest_tasks=0, - data_file=None): - '''Create data to send to InfluxDB server in format SCHEME +def translate( + measure, + json_data, + only_ok, + mapped_fields=True, + standard_fields=True, + longest_tasks=0, + data_file=None, +): + """Create data to send to InfluxDB server in format SCHEME Fields keys are taken from ARA data according to task names. @@ -401,17 +407,18 @@ def translate(measure, json_data, only_ok, :param json_data: JSON data with tasks and times :param: only_ok: boolean, where to count only successful tasks :return: full InfluxDB scheme - ''' + """ data = ast.literal_eval(json_data) data = json.loads(data) tags = InfluxStandardTags() std_fields = InfluxStandardFields() map_fields = InfluxConfiguredFields( - match_map=CUSTOM_MAP, json_data=data, only_ok=only_ok) - longest_fields = InfluxLongestFields(json_data=data, - top=longest_tasks, - only_ok=only_ok) - fields = '' + match_map=CUSTOM_MAP, json_data=data, only_ok=only_ok + ) + longest_fields = InfluxLongestFields( + json_data=data, top=longest_tasks, only_ok=only_ok + ) + fields = "" if standard_fields: fields += std_fields.render() if mapped_fields: @@ -419,33 +426,34 @@ def translate(measure, json_data, only_ok, if longest_tasks: fields += longest_fields.render() if data_file: - sova_fields = SovaFields(os.path.join( - os.path.dirname(data_file), 'failures_file')) + sova_fields = SovaFields( + os.path.join(os.path.dirname(data_file), "failures_file") + ) fields += sova_fields.render() fields = fields.rstrip(",") result = SCHEME.format( measure=measure, tags=tags.render(), fields=fields, - timestamp=std_fields.timestamp() + timestamp=std_fields.timestamp(), ) return result def create_file_with_data(data, path): - '''Create a file with InfluxDB data to send + """Create a file with InfluxDB data to send :param data: data to write :param path: path of the file :return: - ''' + """ with open(path, "a") as f: f.write(data + "\n") def send(file_path, in_url, in_port, in_user, in_pass, in_db): - '''Actual sending of data to InfluxDB server via network + """Actual sending of data to InfluxDB server via network :param file_path: path to file with data to send :param in_url: InfluxDB URL @@ -454,7 +462,7 @@ def send(file_path, in_url, in_port, in_user, in_pass, in_db): :param in_pass: InfluxDB password :param in_db: InfluxDB database name :return: True if sent successfully, otherwise False - ''' + """ import requests # noqa pylint: disable=C0413 from requests.auth import HTTPBasicAuth # noqa pylint: disable=C0413 @@ -465,26 +473,36 @@ def send(file_path, in_url, in_port, in_user, in_pass, in_db): params = {"db": in_db, "precision": "s"} if in_user: if not in_pass: - if os.environ.get('INFLUXDB_PASSWORD'): - with open(os.environ['INFLUXDB_PASSWORD']) as f: + if os.environ.get("INFLUXDB_PASSWORD"): + with open(os.environ["INFLUXDB_PASSWORD"]) as f: in_pass = f.read().strip() else: - return False, 'InfluxDB password was not provided!' + return False, "InfluxDB password was not provided!" auth = HTTPBasicAuth(in_user, in_pass) else: auth = None with open(file_path, "rb") as payload: - req = requests.post(url, params=params, data=payload, auth=auth, - verify=False) + req = requests.post(url, params=params, data=payload, auth=auth, verify=False) if not req or req.status_code != 204: return False, "HTTP: %s\nResponse: %s" % (req.status_code, req.content) - return True, '' + return True, "" -def send_stats(in_url, in_port, in_user, in_pass, in_db, json_data, - measure, data_file, only_ok, mapped_fields=True, - standard_fields=True, longest_tasks=0): - '''Send ARA statistics to InfluxDB server +def send_stats( + in_url, + in_port, + in_user, + in_pass, + in_db, + json_data, + measure, + data_file, + only_ok, + mapped_fields=True, + standard_fields=True, + longest_tasks=0, +): + """Send ARA statistics to InfluxDB server :param in_url: InfluxDB URL :param in_port: InfluxDB port @@ -499,64 +517,72 @@ def send_stats(in_url, in_port, in_user, in_pass, in_db, json_data, :param: standard_fields: if to send standard fields of each job, i.e. times :param: longest_tasks: if to print only longest tasks and how many :return: JSON ansible result - ''' - data2send = translate(measure, json_data, only_ok, mapped_fields, - standard_fields, longest_tasks, data_file) + """ + data2send = translate( + measure, + json_data, + only_ok, + mapped_fields, + standard_fields, + longest_tasks, + data_file, + ) create_file_with_data(data2send, data_file) if in_url: - response, reason = send(data_file, in_url, in_port, in_user, in_pass, - in_db) + response, reason = send(data_file, in_url, in_port, in_user, in_pass, in_db) if not response: return { - 'changed': False, - 'failed': True, - 'influxdb_url': in_url, - 'msg': reason + "changed": False, + "failed": True, + "influxdb_url": in_url, + "msg": reason, } return { - 'changed': True, - 'influxdb_url': in_url, - 'sent_data': data2send, + "changed": True, + "influxdb_url": in_url, + "sent_data": data2send, } else: return { - 'changed': True, - 'data_file': data_file, - 'sent_data': data2send, + "changed": True, + "data_file": data_file, + "sent_data": data2send, } def main(): module = AnsibleModule( # noqa argument_spec=dict( - influxdb_url=dict(required=True, type='str'), - influxdb_port=dict(required=True, type='int'), - influxdb_user=dict(required=False, type='str', default=None), - influxdb_password=dict(required=False, type='str', - default=None, no_log=True), - influxdb_db=dict(required=True, type='str'), - ara_data=dict(required=True, type='str'), - measurement=dict(required=True, type='str'), - data_file=dict(required=True, type='str'), - only_successful_tasks=dict(required=True, type='bool'), - mapped_fields=dict(default=True, type='bool'), - standard_fields=dict(default=True, type='bool'), - longest_tasks=dict(default=0, type='int'), + influxdb_url=dict(required=True, type="str"), + influxdb_port=dict(required=True, type="int"), + influxdb_user=dict(required=False, type="str", default=None), + influxdb_password=dict( + required=False, type="str", default=None, no_log=True + ), + influxdb_db=dict(required=True, type="str"), + ara_data=dict(required=True, type="str"), + measurement=dict(required=True, type="str"), + data_file=dict(required=True, type="str"), + only_successful_tasks=dict(required=True, type="bool"), + mapped_fields=dict(default=True, type="bool"), + standard_fields=dict(default=True, type="bool"), + longest_tasks=dict(default=0, type="int"), ) ) - result = send_stats(module.params['influxdb_url'], - module.params['influxdb_port'], - module.params['influxdb_user'], - module.params['influxdb_password'], - module.params['influxdb_db'], - module.params['ara_data'], - module.params['measurement'], - module.params['data_file'], - module.params['only_successful_tasks'], - module.params['mapped_fields'], - module.params['standard_fields'], - module.params['longest_tasks'], - ) + result = send_stats( + module.params["influxdb_url"], + module.params["influxdb_port"], + module.params["influxdb_user"], + module.params["influxdb_password"], + module.params["influxdb_db"], + module.params["ara_data"], + module.params["measurement"], + module.params["data_file"], + module.params["only_successful_tasks"], + module.params["mapped_fields"], + module.params["standard_fields"], + module.params["longest_tasks"], + ) module.exit_json(**result) diff --git a/plugins/modules/flatten_nested_dict.py b/plugins/modules/flatten_nested_dict.py index 51bb3cd..85bdd2d 100644 --- a/plugins/modules/flatten_nested_dict.py +++ b/plugins/modules/flatten_nested_dict.py @@ -13,13 +13,14 @@ # See the License for the specific language governing permissions and # limitations under the License. from __future__ import absolute_import, division, print_function + __metaclass__ = type ANSIBLE_METADATA = { - 'metadata_version': '0.1', - 'status': ['preview'], - 'supported_by': 'community' + "metadata_version": "0.1", + "status": ["preview"], + "supported_by": "community", } DOCUMENTATION = """ @@ -60,21 +61,15 @@ from ansible.module_utils.basic import AnsibleModule # noqa: E402 def main(): - result = {'data': [], 'changed': False} - module = AnsibleModule( - argument_spec={ - 'data': { - 'type': 'dict', - 'required': True - } - }) + result = {"data": [], "changed": False} + module = AnsibleModule(argument_spec={"data": {"type": "dict", "required": True}}) try: - for group, commands in module.params['data'].items(): + for group, commands in module.params["data"].items(): for cmd_name, cmd_dict in commands.items(): - cmd_dict['name'] = cmd_name - cmd_dict['group'] = group - result['data'].append(cmd_dict) + cmd_dict["name"] = cmd_name + cmd_dict["group"] = group + result["data"].append(cmd_dict) except Exception as e: module.fail_json(msg=str(e)) @@ -82,5 +77,5 @@ def main(): module.exit_json(**result) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/plugins/modules/sova.py b/plugins/modules/sova.py index 3874925..1cfdc6f 100644 --- a/plugins/modules/sova.py +++ b/plugins/modules/sova.py @@ -14,13 +14,14 @@ # limitations under the License. from __future__ import absolute_import, division, print_function + __metaclass__ = type ANSIBLE_METADATA = { - 'metadata_version': '0.1', - 'status': ['preview'], - 'supported_by': 'community' + "metadata_version": "0.1", + "status": ["preview"], + "supported_by": "community", } DOCUMENTATION = """ @@ -109,11 +110,19 @@ file_written: import os # noqa: E402 from copy import deepcopy # noqa: E402 + from ansible.module_utils.basic import AnsibleModule # noqa: E402 def format_msg_filename(text): - for s in (" ", ":", ".", "/", ",", "'", ): + for s in ( + " ", + ":", + ".", + "/", + ",", + "'", + ): text = text.replace(s, "_") return "_" + text.rstrip("_") + ".log" @@ -122,77 +131,82 @@ def main(): module = AnsibleModule( argument_spec=dict( - config=dict(type='dict', default={}), - files=dict(type='dict', required=True), - result=dict(type='path'), - result_file_dir=dict(type='path'), - )) - if not module.params['files']: + config=dict(type="dict", default={}), + files=dict(type="dict", required=True), + result=dict(type="path"), + result_file_dir=dict(type="path"), + ) + ) + if not module.params["files"]: module.fail_json(msg="Files for logs parsing have to be provided!") existing_files = [] - for pattern_file in module.params['files']: - file_ = module.params['files'][pattern_file] + for pattern_file in module.params["files"]: + file_ = module.params["files"][pattern_file] if os.path.exists(file_): existing_files.append(file_) if not existing_files: - results = {"processed_files": [], 'changed': False} + results = {"processed_files": [], "changed": False} module.exit_json(**results) - dict_patterns = deepcopy(module.params['config']) + dict_patterns = deepcopy(module.params["config"]) # from sova_lib import Pattern, parse from ansible.module_utils.sova_lib import Pattern, parse pattern = Pattern(dict_patterns) PATTERNS = pattern.patterns - for name in module.params['files']: + for name in module.params["files"]: if name not in PATTERNS: - module.fail_json(msg="File name %s wasn't found in [%s]" % ( - name, ", ".join(list(PATTERNS.keys())))) + module.fail_json( + msg="File name %s wasn't found in [%s]" + % (name, ", ".join(list(PATTERNS.keys()))) + ) messages, tags = [], [] - for name, file_ in module.params['files'].items(): - if module.params['files'][name] not in existing_files: + for name, file_ in module.params["files"].items(): + if module.params["files"][name] not in existing_files: continue ids, msgs = parse(file_, PATTERNS[name]) - found = [i for i in PATTERNS[name] if i['id'] in ids] - msg_tags = [i['tag'] for i in found if i.get('tag')] + found = [i for i in PATTERNS[name] if i["id"] in ids] + msg_tags = [i["tag"] for i in found if i.get("tag")] messages += msgs tags += msg_tags messages = list(set(messages)) tags = list(set(tags)) - if 'infra' in tags: - reason = 'infra' - elif 'code' in tags: - reason = 'code' + if "infra" in tags: + reason = "infra" + elif "code" in tags: + reason = "code" else: - reason = 'unknown' + reason = "unknown" text = " ".join(messages) or "No failure reason found" file_name = format_msg_filename(text) - result = {'changed': True, "processed_files": existing_files} - result.update({'message': text}) - result.update({'tags': tags}) - if module.params['result'] and messages: + result = {"changed": True, "processed_files": existing_files} + result.update({"message": text}) + result.update({"tags": tags}) + if module.params["result"] and messages: try: - with open(module.params['result'], "w") as f: + with open(module.params["result"], "w") as f: f.write(text + "\n") f.write("Reason: " + reason + "\n") - result.update({'file_written': module.params['result']}) + result.update({"file_written": module.params["result"]}) except Exception as e: module.fail_json( - msg="Can't write result to file %s: %s" % ( - module.params['result'], str(e))) - if module.params['result_file_dir']: - log_file = os.path.join(module.params['result_file_dir'], file_name) + msg="Can't write result to file %s: %s" + % (module.params["result"], str(e)) + ) + if module.params["result_file_dir"]: + log_file = os.path.join(module.params["result_file_dir"], file_name) try: with open(log_file, "w") as f: f.write(text + "\n") f.write("Reason: " + reason + "\n") - result.update({'file_name_written': log_file}) + result.update({"file_name_written": log_file}) except Exception as e: module.fail_json( - msg="Can't write result to file %s: %s" % (log_file, str(e))) + msg="Can't write result to file %s: %s" % (log_file, str(e)) + ) module.exit_json(**result) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5d7bf33 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.isort] +profile = "black" diff --git a/roles/collect_logs/files/heat-deploy-times.py b/roles/collect_logs/files/heat-deploy-times.py index ef9a037..c0ea799 100644 --- a/roles/collect_logs/files/heat-deploy-times.py +++ b/roles/collect_logs/files/heat-deploy-times.py @@ -16,49 +16,47 @@ # Usage: openstack stack event list -f json overcloud | \ # heat-deploy-times.py [list of resource names] # If no resource names are provided, all of the resources will be output. -from __future__ import (absolute_import, division, print_function) +from __future__ import absolute_import, division, print_function + import json import sys import time - __metaclass__ = type def process_events(all_events, events): times = {} for event in all_events: - name = event['resource_name'] - status = event['resource_status'] + name = event["resource_name"] + status = event["resource_status"] # Older clients return timestamps in the first format, newer ones # append a Z. This way we can handle both formats. try: - strptime = time.strptime(event['event_time'], - '%Y-%m-%dT%H:%M:%S') + strptime = time.strptime(event["event_time"], "%Y-%m-%dT%H:%M:%S") except ValueError: - strptime = time.strptime(event['event_time'], - '%Y-%m-%dT%H:%M:%SZ') + strptime = time.strptime(event["event_time"], "%Y-%m-%dT%H:%M:%SZ") etime = time.mktime(strptime) if name in events: - if status == 'CREATE_IN_PROGRESS': - times[name] = {'start': etime, 'elapsed': None} - elif status == 'CREATE_COMPLETE': - times[name]['elapsed'] = etime - times[name]['start'] - for name, data in sorted(times.items(), - key=lambda x: x[1]['elapsed'], - reverse=True): - elapsed = 'Still in progress' - if times[name]['elapsed'] is not None: - elapsed = times[name]['elapsed'] - print('%s %s') % (name, elapsed) + if status == "CREATE_IN_PROGRESS": + times[name] = {"start": etime, "elapsed": None} + elif status == "CREATE_COMPLETE": + times[name]["elapsed"] = etime - times[name]["start"] + for name, data in sorted( + times.items(), key=lambda x: x[1]["elapsed"], reverse=True + ): + elapsed = "Still in progress" + if times[name]["elapsed"] is not None: + elapsed = times[name]["elapsed"] + print("%s %s") % (name, elapsed) -if __name__ == '__main__': +if __name__ == "__main__": stdin = sys.stdin.read() all_events = json.loads(stdin) events = sys.argv[1:] if not events: events = set() for event in all_events: - events.add(event['resource_name']) + events.add(event["resource_name"]) process_events(all_events, events) diff --git a/roles/collect_logs/module_utils/sova_lib.py b/roles/collect_logs/module_utils/sova_lib.py index ed5b283..87f828a 100644 --- a/roles/collect_logs/module_utils/sova_lib.py +++ b/roles/collect_logs/module_utils/sova_lib.py @@ -11,9 +11,11 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import (absolute_import, division, print_function) +from __future__ import absolute_import, division, print_function + import gzip import logging + import yaml try: @@ -24,9 +26,12 @@ except ImportError: __metaclass__ = type logging.basicConfig( - format=('%(asctime)s - %(name)s - %(levelname)s - ' - '%(module)s.%(funcName)s:%(lineno)d - %(message)s')) -log = logging.getLogger('parser') + format=( + "%(asctime)s - %(name)s - %(levelname)s - " + "%(module)s.%(funcName)s:%(lineno)d - %(message)s" + ) +) +log = logging.getLogger("parser") log.setLevel(logging.ERROR) @@ -46,22 +51,23 @@ class Pattern(object): def setup_regexes(self): self.regexes = {} if self.config: - for regexp in self.config.get('regexes', []): + for regexp in self.config.get("regexes", []): flags = [] - if regexp.get('multiline'): + if regexp.get("multiline"): flags.append(regex_module.MULTILINE) - self.regexes[regexp.get('name')] = regex_module.compile( - r'{0}'.format(regexp.get('regex')), *flags) + self.regexes[regexp.get("name")] = regex_module.compile( + r"{0}".format(regexp.get("regex")), *flags + ) def setup_patterns(self): - self._patterns = self.config.get('patterns', {}) + self._patterns = self.config.get("patterns", {}) if self._patterns: for key in self._patterns: for p in self._patterns[key]: - if p['pattern'] in self.regexes: - p['pattern'] = self.regexes[p['pattern']] - if p['logstash'] in self.regexes: - p['logstash'] = self.regexes[p['logstash']] + if p["pattern"] in self.regexes: + p["pattern"] = self.regexes[p["pattern"]] + if p["logstash"] in self.regexes: + p["logstash"] = self.regexes[p["logstash"]] @property def patterns(self): @@ -92,12 +98,9 @@ def parse(text_file, patterns): with open_func(text_file, "rt") as finput: text = finput.read() for p in patterns: - line_matched = line_match( - p["pattern"], text, exclude=p.get("exclude")) + line_matched = line_match(p["pattern"], text, exclude=p.get("exclude")) if line_matched: - log.debug( - "Found pattern %s in file %s", - repr(p), text_file) + log.debug("Found pattern %s in file %s", repr(p), text_file) ids.append(p["id"]) msgs.append(p["msg"].format(line_matched)) return list(set(ids)), list(set(msgs)) diff --git a/setup.cfg b/setup.cfg index 39caddd..02ae8f6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,8 +34,9 @@ skip_authors = True skip_changelog = True [flake8] -# E123, E125 skipped as they are invalid PEP-8. -# E265 deals with spaces inside of comments +# black compatible settings +# https://black.readthedocs.io/en/stable/the_black_code_style.html +max-line-length = 88 +extend-ignore = E203,E501,W503 show-source = True -ignore = E123,E125,E265 builtins = _ diff --git a/setup.py b/setup.py index 79e540d..bf2f6f4 100644 --- a/setup.py +++ b/setup.py @@ -12,12 +12,11 @@ # License for the specific language governing permissions and limitations # under the License. -from __future__ import (absolute_import, division, print_function) +from __future__ import absolute_import, division, print_function + import setuptools __metaclass__ = type -setuptools.setup( - setup_requires=['pbr'], - pbr=True) +setuptools.setup(setup_requires=["pbr"], pbr=True) diff --git a/tests/common/utils.py b/tests/common/utils.py index 589a66c..0aa90be 100644 --- a/tests/common/utils.py +++ b/tests/common/utils.py @@ -1,21 +1,21 @@ from __future__ import absolute_import, division, print_function + __metaclass__ = type import json +from unittest.mock import patch from ansible.module_utils import basic from ansible.module_utils._text import to_bytes -from unittest.mock import patch - def set_module_args(**args): - if '_ansible_remote_tmp' not in args: - args['_ansible_remote_tmp'] = '/tmp' - if '_ansible_keep_remote_files' not in args: - args['_ansible_keep_remote_files'] = False + if "_ansible_remote_tmp" not in args: + args["_ansible_remote_tmp"] = "/tmp" + if "_ansible_keep_remote_files" not in args: + args["_ansible_keep_remote_files"] = False - args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) + args = json.dumps({"ANSIBLE_MODULE_ARGS": args}) basic._ANSIBLE_ARGS = to_bytes(args) @@ -28,20 +28,22 @@ class AnsibleFailJson(Exception): def exit_json(*args, **kwargs): - if 'changed' not in kwargs: - kwargs['changed'] = False + if "changed" not in kwargs: + kwargs["changed"] = False raise AnsibleExitJson(kwargs) def fail_json(*args, **kwargs): - kwargs['failed'] = True + kwargs["failed"] = True raise AnsibleFailJson(kwargs) class ModuleTestCase: def setup_method(self): self.mock_module = patch.multiple( - basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json, + basic.AnsibleModule, + exit_json=exit_json, + fail_json=fail_json, ) self.mock_module.start() @@ -50,4 +52,4 @@ class ModuleTestCase: def generate_name(test_case): - return test_case['name'] + return test_case["name"] diff --git a/tests/test_flatten_nested_dict.py b/tests/test_flatten_nested_dict.py index b203af0..3776836 100644 --- a/tests/test_flatten_nested_dict.py +++ b/tests/test_flatten_nested_dict.py @@ -1,12 +1,16 @@ -from __future__ import (absolute_import, division, print_function) -import pytest # noqa +from __future__ import absolute_import, division, print_function + import os import sys -from common.utils import ( - AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args, -) -import yaml +import pytest # noqa +import yaml +from common.utils import ( + AnsibleExitJson, + AnsibleFailJson, + ModuleTestCase, + set_module_args, +) __metaclass__ = type SAMPLE_INPUT_1 = """ @@ -33,15 +37,14 @@ import flatten_nested_dict # noqa: E402 class TestFlattenNestedDict(ModuleTestCase): - def test_invalid_args(self): set_module_args( data="invalid", ) with pytest.raises(AnsibleFailJson) as context: flatten_nested_dict.main() - assert context.value.args[0]['failed'] is True - assert 'msg' in context.value.args[0] + assert context.value.args[0]["failed"] is True + assert "msg" in context.value.args[0] def test_empty(self): set_module_args( @@ -49,14 +52,11 @@ class TestFlattenNestedDict(ModuleTestCase): ) with pytest.raises(AnsibleExitJson) as context: flatten_nested_dict.main() - assert context.value.args[0] == {'data': [], 'changed': False} + assert context.value.args[0] == {"data": [], "changed": False} def test_one(self): - set_module_args( - data=yaml.safe_load(SAMPLE_INPUT_1)['data'] - ) + set_module_args(data=yaml.safe_load(SAMPLE_INPUT_1)["data"]) with pytest.raises(AnsibleExitJson) as context: flatten_nested_dict.main() - assert context.value.args[0]['changed'] is False - assert context.value.args[0]['data'] == \ - yaml.safe_load(SAMPLE_OUTPUT_1)['data'] + assert context.value.args[0]["changed"] is False + assert context.value.args[0]["data"] == yaml.safe_load(SAMPLE_OUTPUT_1)["data"]