| """Utilities for working with puppet tools.""" |
| |
| from __future__ import absolute_import |
| import collections |
| import re |
| |
| _ANSI_COLOR_ESCAPE_SEQUENCE = r'\x1b[^m]*m' |
| |
| |
| def parser_errors_to_comments(errors, directory): |
| """Convert errors reported by the puppet parser in Gerrit comments. |
| |
| An example of an error from the puppet parser validator looks like: |
| Error: Could not parse for environment prod: |
| Syntax error at ','; expected '}' at /path/to/puppet/file.pp:27 |
| |
| Args: |
| errors: List of stderr strings output from the pupper parser. |
| directory: The directory to strip off of any filenames so file comments |
| are rooted at the project directory that Gerrit expects. |
| |
| Returns: |
| Dict of Gerrit comments: |
| { 'filename': [{'line': '27', 'message': 'Error!'}, ...], ... } |
| """ |
| |
| comments = collections.defaultdict(list) |
| for error in errors: |
| error = re.sub(_ANSI_COLOR_ESCAPE_SEQUENCE, '', error) |
| error_parts = error.split() |
| error_location = error_parts[-1] # /path/to/puppet/file.pp:27 |
| abs_filename, line_number = error_location.split(':') |
| rel_filename_start = abs_filename.index(directory) + len(directory) |
| rel_filename = abs_filename[rel_filename_start:] |
| if rel_filename.startswith('/'): |
| rel_filename = rel_filename[1:] |
| |
| error_parts[-1] = '{}:{}'.format(rel_filename, line_number) |
| |
| comment = {'line': line_number, 'message': ' '.join(error_parts)} |
| comments[rel_filename].append(comment) |
| |
| return comments |
| |
| |
| def linter_errors_to_comments(errors, directory): |
| """Convert errors reported by puppet-lint in Gerrit comments. |
| |
| Example json error: |
| { |
| "message": "indentation of => is not properly aligned", |
| "line": 6, |
| "column": 12, |
| "token": "#<PuppetLint::Lexer::Token:0x00000001d885a0>", |
| "indent_depth": 13, |
| "newline": false, |
| "newline_indent": " ", |
| "kind": "warning", |
| "check": "arrow_alignment", |
| "fullpath": "/abs/path/to/puppet/manifests/common.pp", |
| "path": "manifests/common.pp", |
| "filename": "common.pp", |
| "KIND": "WARNING" |
| } |
| |
| |
| Args: |
| errors: List of error dicts returned from puppet-lint with --json |
| directory: The directory to strip off of any filenames so file comments |
| are rooted at the project directory that Gerrit expects. |
| |
| Returns: |
| Dict of Gerrit comments: |
| { 'filename': [{'line': '27', 'message': 'Error!'}, ...], ... } |
| """ |
| if not errors: |
| return {} |
| |
| # puppet-lint output can wrap errors in nested lists, e.g. |
| # [[error1, error2, ...]], so handle that gracefully. |
| if isinstance(errors[0], list): |
| errors = errors[0] |
| |
| comments = collections.defaultdict(list) |
| for error in errors: |
| comment = {'line': str(error['line']), 'message': error['message']} |
| rel_filename_start = error['path'].index(directory) + len(directory) |
| rel_filename = error['path'][rel_filename_start:] |
| if rel_filename.startswith('/'): |
| rel_filename = rel_filename[1:] |
| comments[rel_filename].append(comment) |
| return comments |
| |
| |
| def get_cron_entries(puppet_lines): |
| """Pull out cron entries from a puppet file. |
| |
| A cron entry is converted into a dict, where the keys are the attributes |
| and the values are the attribute values (as strings). |
| |
| In addition, the dict as a special '_line_number' attribute that records |
| the line number the cron entry started at. |
| |
| Example |
| # Puppet entry # As dict |
| 27: cron { 'my_cron': cron_entry = { |
| 28: ensure => present, 'ensure': 'present', |
| 29: hour => 2, ----> 'hour': '2', |
| 30: minute => 0, 'minute': '0', |
| 31: command => $some_script, 'command': '$some_script', |
| '_line_number': '27', |
| 32: } } |
| |
| Args: |
| puppet_lines: List of lines from a puppet_file (e.g. from readlines()). |
| |
| Returns: |
| List of dict objects representing the cron entries. |
| """ |
| cron_entries = [] |
| within_cron_entry = False |
| curly_brace_count = 0 |
| for line_number, line in enumerate(puppet_lines, start=1): |
| if 'cron {' in line: |
| within_cron_entry = True |
| curly_brace_count = 1 |
| cron_entry = {'_line_number': str(line_number)} |
| continue |
| |
| if not within_cron_entry: |
| continue |
| |
| curly_brace_count += line.count('{') - line.count('}') |
| if curly_brace_count == 0: |
| within_cron_entry = False |
| cron_entries.append(cron_entry) |
| cron_entry = {} |
| continue |
| |
| if '=>' in line: |
| attribute, value = line.split('=>', 1) |
| if value.rstrip().endswith(','): |
| value = value.rstrip()[:-1] |
| cron_entry[attribute.strip()] = value.lstrip() |
| |
| return cron_entries |
| |
| |
| def cron_errors_to_comments(errors): |
| """Converts |errors| into Gerrit-style comments. |
| |
| Args: |
| errors: List of (filename, cron_entry dict) |
| Returns: |
| Dict of Gerrit comments: |
| { 'filename': [{'line': '27', 'message': 'Error!'}, ...], ... } |
| """ |
| comments = collections.defaultdict(list) |
| for puppet_file, cron_entry in errors: |
| comment = {'line': cron_entry['_line_number'], |
| 'message': 'cron entry missing "minute" attribute.',} |
| comments[puppet_file].append(comment) |
| return comments |
| |
| |