summaryrefslogtreecommitdiffstats
path: root/lib/ansible/errors
diff options
context:
space:
mode:
Diffstat (limited to 'lib/ansible/errors')
-rw-r--r--lib/ansible/errors/__init__.py373
-rw-r--r--lib/ansible/errors/yaml_strings.py140
2 files changed, 513 insertions, 0 deletions
diff --git a/lib/ansible/errors/__init__.py b/lib/ansible/errors/__init__.py
new file mode 100644
index 0000000..a113225
--- /dev/null
+++ b/lib/ansible/errors/__init__.py
@@ -0,0 +1,373 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import re
+import traceback
+
+from collections.abc import Sequence
+
+from ansible.errors.yaml_strings import (
+ YAML_COMMON_DICT_ERROR,
+ YAML_COMMON_LEADING_TAB_ERROR,
+ YAML_COMMON_PARTIALLY_QUOTED_LINE_ERROR,
+ YAML_COMMON_UNBALANCED_QUOTES_ERROR,
+ YAML_COMMON_UNQUOTED_COLON_ERROR,
+ YAML_COMMON_UNQUOTED_VARIABLE_ERROR,
+ YAML_POSITION_DETAILS,
+ YAML_AND_SHORTHAND_ERROR,
+)
+from ansible.module_utils._text import to_native, to_text
+
+
+class AnsibleError(Exception):
+ '''
+ This is the base class for all errors raised from Ansible code,
+ and can be instantiated with two optional parameters beyond the
+ error message to control whether detailed information is displayed
+ when the error occurred while parsing a data file of some kind.
+
+ Usage:
+
+ raise AnsibleError('some message here', obj=obj, show_content=True)
+
+ Where "obj" is some subclass of ansible.parsing.yaml.objects.AnsibleBaseYAMLObject,
+ which should be returned by the DataLoader() class.
+ '''
+
+ def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None):
+ super(AnsibleError, self).__init__(message)
+
+ self._show_content = show_content
+ self._suppress_extended_error = suppress_extended_error
+ self._message = to_native(message)
+ self.obj = obj
+ self.orig_exc = orig_exc
+
+ @property
+ def message(self):
+ # we import this here to prevent an import loop problem,
+ # since the objects code also imports ansible.errors
+ from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject
+
+ message = [self._message]
+ if isinstance(self.obj, AnsibleBaseYAMLObject):
+ extended_error = self._get_extended_error()
+ if extended_error and not self._suppress_extended_error:
+ message.append(
+ '\n\n%s' % to_native(extended_error)
+ )
+ elif self.orig_exc:
+ message.append('. %s' % to_native(self.orig_exc))
+
+ return ''.join(message)
+
+ @message.setter
+ def message(self, val):
+ self._message = val
+
+ def __str__(self):
+ return self.message
+
+ def __repr__(self):
+ return self.message
+
+ def _get_error_lines_from_file(self, file_name, line_number):
+ '''
+ Returns the line in the file which corresponds to the reported error
+ location, as well as the line preceding it (if the error did not
+ occur on the first line), to provide context to the error.
+ '''
+
+ target_line = ''
+ prev_line = ''
+
+ with open(file_name, 'r') as f:
+ lines = f.readlines()
+
+ # In case of a YAML loading error, PyYAML will report the very last line
+ # as the location of the error. Avoid an index error here in order to
+ # return a helpful message.
+ file_length = len(lines)
+ if line_number >= file_length:
+ line_number = file_length - 1
+
+ # If target_line contains only whitespace, move backwards until
+ # actual code is found. If there are several empty lines after target_line,
+ # the error lines would just be blank, which is not very helpful.
+ target_line = lines[line_number]
+ while not target_line.strip():
+ line_number -= 1
+ target_line = lines[line_number]
+
+ if line_number > 0:
+ prev_line = lines[line_number - 1]
+
+ return (target_line, prev_line)
+
+ def _get_extended_error(self):
+ '''
+ Given an object reporting the location of the exception in a file, return
+ detailed information regarding it including:
+
+ * the line which caused the error as well as the one preceding it
+ * causes and suggested remedies for common syntax errors
+
+ If this error was created with show_content=False, the reporting of content
+ is suppressed, as the file contents may be sensitive (ie. vault data).
+ '''
+
+ error_message = ''
+
+ try:
+ (src_file, line_number, col_number) = self.obj.ansible_pos
+ error_message += YAML_POSITION_DETAILS % (src_file, line_number, col_number)
+ if src_file not in ('<string>', '<unicode>') and self._show_content:
+ (target_line, prev_line) = self._get_error_lines_from_file(src_file, line_number - 1)
+ target_line = to_text(target_line)
+ prev_line = to_text(prev_line)
+ if target_line:
+ stripped_line = target_line.replace(" ", "")
+
+ # Check for k=v syntax in addition to YAML syntax and set the appropriate error position,
+ # arrow index
+ if re.search(r'\w+(\s+)?=(\s+)?[\w/-]+', prev_line):
+ error_position = prev_line.rstrip().find('=')
+ arrow_line = (" " * error_position) + "^ here"
+ error_message = YAML_POSITION_DETAILS % (src_file, line_number - 1, error_position + 1)
+ error_message += "\nThe offending line appears to be:\n\n%s\n%s\n\n" % (prev_line.rstrip(), arrow_line)
+ error_message += YAML_AND_SHORTHAND_ERROR
+ else:
+ arrow_line = (" " * (col_number - 1)) + "^ here"
+ error_message += "\nThe offending line appears to be:\n\n%s\n%s\n%s\n" % (prev_line.rstrip(), target_line.rstrip(), arrow_line)
+
+ # TODO: There may be cases where there is a valid tab in a line that has other errors.
+ if '\t' in target_line:
+ error_message += YAML_COMMON_LEADING_TAB_ERROR
+ # common error/remediation checking here:
+ # check for unquoted vars starting lines
+ if ('{{' in target_line and '}}' in target_line) and ('"{{' not in target_line or "'{{" not in target_line):
+ error_message += YAML_COMMON_UNQUOTED_VARIABLE_ERROR
+ # check for common dictionary mistakes
+ elif ":{{" in stripped_line and "}}" in stripped_line:
+ error_message += YAML_COMMON_DICT_ERROR
+ # check for common unquoted colon mistakes
+ elif (len(target_line) and
+ len(target_line) > 1 and
+ len(target_line) > col_number and
+ target_line[col_number] == ":" and
+ target_line.count(':') > 1):
+ error_message += YAML_COMMON_UNQUOTED_COLON_ERROR
+ # otherwise, check for some common quoting mistakes
+ else:
+ # FIXME: This needs to split on the first ':' to account for modules like lineinfile
+ # that may have lines that contain legitimate colons, e.g., line: 'i ALL= (ALL) NOPASSWD: ALL'
+ # and throw off the quote matching logic.
+ parts = target_line.split(":")
+ if len(parts) > 1:
+ middle = parts[1].strip()
+ match = False
+ unbalanced = False
+
+ if middle.startswith("'") and not middle.endswith("'"):
+ match = True
+ elif middle.startswith('"') and not middle.endswith('"'):
+ match = True
+
+ if (len(middle) > 0 and
+ middle[0] in ['"', "'"] and
+ middle[-1] in ['"', "'"] and
+ target_line.count("'") > 2 or
+ target_line.count('"') > 2):
+ unbalanced = True
+
+ if match:
+ error_message += YAML_COMMON_PARTIALLY_QUOTED_LINE_ERROR
+ if unbalanced:
+ error_message += YAML_COMMON_UNBALANCED_QUOTES_ERROR
+
+ except (IOError, TypeError):
+ error_message += '\n(could not open file to display line)'
+ except IndexError:
+ error_message += '\n(specified line no longer in file, maybe it changed?)'
+
+ return error_message
+
+
+class AnsibleAssertionError(AnsibleError, AssertionError):
+ '''Invalid assertion'''
+ pass
+
+
+class AnsibleOptionsError(AnsibleError):
+ ''' bad or incomplete options passed '''
+ pass
+
+
+class AnsibleParserError(AnsibleError):
+ ''' something was detected early that is wrong about a playbook or data file '''
+ pass
+
+
+class AnsibleInternalError(AnsibleError):
+ ''' internal safeguards tripped, something happened in the code that should never happen '''
+ pass
+
+
+class AnsibleRuntimeError(AnsibleError):
+ ''' ansible had a problem while running a playbook '''
+ pass
+
+
+class AnsibleModuleError(AnsibleRuntimeError):
+ ''' a module failed somehow '''
+ pass
+
+
+class AnsibleConnectionFailure(AnsibleRuntimeError):
+ ''' the transport / connection_plugin had a fatal error '''
+ pass
+
+
+class AnsibleAuthenticationFailure(AnsibleConnectionFailure):
+ '''invalid username/password/key'''
+ pass
+
+
+class AnsibleCallbackError(AnsibleRuntimeError):
+ ''' a callback failure '''
+ pass
+
+
+class AnsibleTemplateError(AnsibleRuntimeError):
+ '''A template related error'''
+ pass
+
+
+class AnsibleFilterError(AnsibleTemplateError):
+ ''' a templating failure '''
+ pass
+
+
+class AnsibleLookupError(AnsibleTemplateError):
+ ''' a lookup failure '''
+ pass
+
+
+class AnsibleUndefinedVariable(AnsibleTemplateError):
+ ''' a templating failure '''
+ pass
+
+
+class AnsibleFileNotFound(AnsibleRuntimeError):
+ ''' a file missing failure '''
+
+ def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None, paths=None, file_name=None):
+
+ self.file_name = file_name
+ self.paths = paths
+
+ if message:
+ message += "\n"
+ if self.file_name:
+ message += "Could not find or access '%s'" % to_text(self.file_name)
+ else:
+ message += "Could not find file"
+
+ if self.paths and isinstance(self.paths, Sequence):
+ searched = to_text('\n\t'.join(self.paths))
+ if message:
+ message += "\n"
+ message += "Searched in:\n\t%s" % searched
+
+ message += " on the Ansible Controller.\nIf you are using a module and expect the file to exist on the remote, see the remote_src option"
+
+ super(AnsibleFileNotFound, self).__init__(message=message, obj=obj, show_content=show_content,
+ suppress_extended_error=suppress_extended_error, orig_exc=orig_exc)
+
+
+# These Exceptions are temporary, using them as flow control until we can get a better solution.
+# DO NOT USE as they will probably be removed soon.
+# We will port the action modules in our tree to use a context manager instead.
+class AnsibleAction(AnsibleRuntimeError):
+ ''' Base Exception for Action plugin flow control '''
+
+ def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None, result=None):
+
+ super(AnsibleAction, self).__init__(message=message, obj=obj, show_content=show_content,
+ suppress_extended_error=suppress_extended_error, orig_exc=orig_exc)
+ if result is None:
+ self.result = {}
+ else:
+ self.result = result
+
+
+class AnsibleActionSkip(AnsibleAction):
+ ''' an action runtime skip'''
+
+ def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None, result=None):
+ super(AnsibleActionSkip, self).__init__(message=message, obj=obj, show_content=show_content,
+ suppress_extended_error=suppress_extended_error, orig_exc=orig_exc, result=result)
+ self.result.update({'skipped': True, 'msg': message})
+
+
+class AnsibleActionFail(AnsibleAction):
+ ''' an action runtime failure'''
+ def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None, result=None):
+ super(AnsibleActionFail, self).__init__(message=message, obj=obj, show_content=show_content,
+ suppress_extended_error=suppress_extended_error, orig_exc=orig_exc, result=result)
+ self.result.update({'failed': True, 'msg': message, 'exception': traceback.format_exc()})
+
+
+class _AnsibleActionDone(AnsibleAction):
+ ''' an action runtime early exit'''
+ pass
+
+
+class AnsiblePluginError(AnsibleError):
+ ''' base class for Ansible plugin-related errors that do not need AnsibleError contextual data '''
+ def __init__(self, message=None, plugin_load_context=None):
+ super(AnsiblePluginError, self).__init__(message)
+ self.plugin_load_context = plugin_load_context
+
+
+class AnsiblePluginRemovedError(AnsiblePluginError):
+ ''' a requested plugin has been removed '''
+ pass
+
+
+class AnsiblePluginCircularRedirect(AnsiblePluginError):
+ '''a cycle was detected in plugin redirection'''
+ pass
+
+
+class AnsibleCollectionUnsupportedVersionError(AnsiblePluginError):
+ '''a collection is not supported by this version of Ansible'''
+ pass
+
+
+class AnsibleFilterTypeError(AnsibleTemplateError, TypeError):
+ ''' a Jinja filter templating failure due to bad type'''
+ pass
+
+
+class AnsiblePluginNotFound(AnsiblePluginError):
+ ''' Indicates we did not find an Ansible plugin '''
+ pass
diff --git a/lib/ansible/errors/yaml_strings.py b/lib/ansible/errors/yaml_strings.py
new file mode 100644
index 0000000..e10a3f9
--- /dev/null
+++ b/lib/ansible/errors/yaml_strings.py
@@ -0,0 +1,140 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+__all__ = [
+ 'YAML_SYNTAX_ERROR',
+ 'YAML_POSITION_DETAILS',
+ 'YAML_COMMON_DICT_ERROR',
+ 'YAML_COMMON_UNQUOTED_VARIABLE_ERROR',
+ 'YAML_COMMON_UNQUOTED_COLON_ERROR',
+ 'YAML_COMMON_PARTIALLY_QUOTED_LINE_ERROR',
+ 'YAML_COMMON_UNBALANCED_QUOTES_ERROR',
+]
+
+YAML_SYNTAX_ERROR = """\
+Syntax Error while loading YAML.
+ %s"""
+
+YAML_POSITION_DETAILS = """\
+The error appears to be in '%s': line %s, column %s, but may
+be elsewhere in the file depending on the exact syntax problem.
+"""
+
+YAML_COMMON_DICT_ERROR = """\
+This one looks easy to fix. YAML thought it was looking for the start of a
+hash/dictionary and was confused to see a second "{". Most likely this was
+meant to be an ansible template evaluation instead, so we have to give the
+parser a small hint that we wanted a string instead. The solution here is to
+just quote the entire value.
+
+For instance, if the original line was:
+
+ app_path: {{ base_path }}/foo
+
+It should be written as:
+
+ app_path: "{{ base_path }}/foo"
+"""
+
+YAML_COMMON_UNQUOTED_VARIABLE_ERROR = """\
+We could be wrong, but this one looks like it might be an issue with
+missing quotes. Always quote template expression brackets when they
+start a value. For instance:
+
+ with_items:
+ - {{ foo }}
+
+Should be written as:
+
+ with_items:
+ - "{{ foo }}"
+"""
+
+YAML_COMMON_UNQUOTED_COLON_ERROR = """\
+This one looks easy to fix. There seems to be an extra unquoted colon in the line
+and this is confusing the parser. It was only expecting to find one free
+colon. The solution is just add some quotes around the colon, or quote the
+entire line after the first colon.
+
+For instance, if the original line was:
+
+ copy: src=file.txt dest=/path/filename:with_colon.txt
+
+It can be written as:
+
+ copy: src=file.txt dest='/path/filename:with_colon.txt'
+
+Or:
+
+ copy: 'src=file.txt dest=/path/filename:with_colon.txt'
+"""
+
+YAML_COMMON_PARTIALLY_QUOTED_LINE_ERROR = """\
+This one looks easy to fix. It seems that there is a value started
+with a quote, and the YAML parser is expecting to see the line ended
+with the same kind of quote. For instance:
+
+ when: "ok" in result.stdout
+
+Could be written as:
+
+ when: '"ok" in result.stdout'
+
+Or equivalently:
+
+ when: "'ok' in result.stdout"
+"""
+
+YAML_COMMON_UNBALANCED_QUOTES_ERROR = """\
+We could be wrong, but this one looks like it might be an issue with
+unbalanced quotes. If starting a value with a quote, make sure the
+line ends with the same set of quotes. For instance this arbitrary
+example:
+
+ foo: "bad" "wolf"
+
+Could be written as:
+
+ foo: '"bad" "wolf"'
+"""
+
+YAML_COMMON_LEADING_TAB_ERROR = """\
+There appears to be a tab character at the start of the line.
+
+YAML does not use tabs for formatting. Tabs should be replaced with spaces.
+
+For example:
+ - name: update tooling
+ vars:
+ version: 1.2.3
+# ^--- there is a tab there.
+
+Should be written as:
+ - name: update tooling
+ vars:
+ version: 1.2.3
+# ^--- all spaces here.
+"""
+
+YAML_AND_SHORTHAND_ERROR = """\
+There appears to be both 'k=v' shorthand syntax and YAML in this task. \
+Only one syntax may be used.
+"""