Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/ansible_core-2.17.0.dev0-py3.8.egg/ansible/errors/__init__.py: 43%

168 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-11-30 06:38 +0000

1# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com> 

2# 

3# This file is part of Ansible 

4# 

5# Ansible is free software: you can redistribute it and/or modify 

6# it under the terms of the GNU General Public License as published by 

7# the Free Software Foundation, either version 3 of the License, or 

8# (at your option) any later version. 

9# 

10# Ansible is distributed in the hope that it will be useful, 

11# but WITHOUT ANY WARRANTY; without even the implied warranty of 

12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

13# GNU General Public License for more details. 

14# 

15# You should have received a copy of the GNU General Public License 

16# along with Ansible. If not, see <http://www.gnu.org/licenses/>. 

17 

18from __future__ import annotations 

19 

20import re 

21import traceback 

22 

23from collections.abc import Sequence 

24 

25from ansible.errors.yaml_strings import ( 

26 YAML_COMMON_DICT_ERROR, 

27 YAML_COMMON_LEADING_TAB_ERROR, 

28 YAML_COMMON_PARTIALLY_QUOTED_LINE_ERROR, 

29 YAML_COMMON_UNBALANCED_QUOTES_ERROR, 

30 YAML_COMMON_UNQUOTED_COLON_ERROR, 

31 YAML_COMMON_UNQUOTED_VARIABLE_ERROR, 

32 YAML_POSITION_DETAILS, 

33 YAML_AND_SHORTHAND_ERROR, 

34) 

35from ansible.module_utils.common.text.converters import to_native, to_text 

36 

37 

38class AnsibleError(Exception): 

39 ''' 

40 This is the base class for all errors raised from Ansible code, 

41 and can be instantiated with two optional parameters beyond the 

42 error message to control whether detailed information is displayed 

43 when the error occurred while parsing a data file of some kind. 

44 

45 Usage: 

46 

47 raise AnsibleError('some message here', obj=obj, show_content=True) 

48 

49 Where "obj" is some subclass of ansible.parsing.yaml.objects.AnsibleBaseYAMLObject, 

50 which should be returned by the DataLoader() class. 

51 ''' 

52 

53 def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None): 

54 super(AnsibleError, self).__init__(message) 

55 

56 self._show_content = show_content 

57 self._suppress_extended_error = suppress_extended_error 

58 self._message = to_native(message) 

59 self.obj = obj 

60 self.orig_exc = orig_exc 

61 

62 @property 

63 def message(self): 

64 # we import this here to prevent an import loop problem, 

65 # since the objects code also imports ansible.errors 

66 from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject 

67 

68 message = [self._message] 

69 if isinstance(self.obj, AnsibleBaseYAMLObject): 

70 extended_error = self._get_extended_error() 

71 if extended_error and not self._suppress_extended_error: 

72 message.append( 

73 '\n\n%s' % to_native(extended_error) 

74 ) 

75 elif self.orig_exc: 

76 message.append('. %s' % to_native(self.orig_exc)) 

77 

78 return ''.join(message) 

79 

80 @message.setter 

81 def message(self, val): 

82 self._message = val 

83 

84 def __str__(self): 

85 return self.message 

86 

87 def __repr__(self): 

88 return self.message 

89 

90 def _get_error_lines_from_file(self, file_name, line_number): 

91 ''' 

92 Returns the line in the file which corresponds to the reported error 

93 location, as well as the line preceding it (if the error did not 

94 occur on the first line), to provide context to the error. 

95 ''' 

96 

97 target_line = '' 

98 prev_line = '' 

99 

100 with open(file_name, 'r') as f: 

101 lines = f.readlines() 

102 

103 # In case of a YAML loading error, PyYAML will report the very last line 

104 # as the location of the error. Avoid an index error here in order to 

105 # return a helpful message. 

106 file_length = len(lines) 

107 if line_number >= file_length: 

108 line_number = file_length - 1 

109 

110 # If target_line contains only whitespace, move backwards until 

111 # actual code is found. If there are several empty lines after target_line, 

112 # the error lines would just be blank, which is not very helpful. 

113 target_line = lines[line_number] 

114 while not target_line.strip(): 

115 line_number -= 1 

116 target_line = lines[line_number] 

117 

118 if line_number > 0: 

119 prev_line = lines[line_number - 1] 

120 

121 return (target_line, prev_line) 

122 

123 def _get_extended_error(self): 

124 ''' 

125 Given an object reporting the location of the exception in a file, return 

126 detailed information regarding it including: 

127 

128 * the line which caused the error as well as the one preceding it 

129 * causes and suggested remedies for common syntax errors 

130 

131 If this error was created with show_content=False, the reporting of content 

132 is suppressed, as the file contents may be sensitive (ie. vault data). 

133 ''' 

134 

135 error_message = '' 

136 

137 try: 

138 (src_file, line_number, col_number) = self.obj.ansible_pos 

139 error_message += YAML_POSITION_DETAILS % (src_file, line_number, col_number) 

140 if src_file not in ('<string>', '<unicode>') and self._show_content: 

141 (target_line, prev_line) = self._get_error_lines_from_file(src_file, line_number - 1) 

142 target_line = to_text(target_line) 

143 prev_line = to_text(prev_line) 

144 if target_line: 

145 stripped_line = target_line.replace(" ", "") 

146 

147 # Check for k=v syntax in addition to YAML syntax and set the appropriate error position, 

148 # arrow index 

149 if re.search(r'\w+(\s+)?=(\s+)?[\w/-]+', prev_line): 

150 error_position = prev_line.rstrip().find('=') 

151 arrow_line = (" " * error_position) + "^ here" 

152 error_message = YAML_POSITION_DETAILS % (src_file, line_number - 1, error_position + 1) 

153 error_message += "\nThe offending line appears to be:\n\n%s\n%s\n\n" % (prev_line.rstrip(), arrow_line) 

154 error_message += YAML_AND_SHORTHAND_ERROR 

155 else: 

156 arrow_line = (" " * (col_number - 1)) + "^ here" 

157 error_message += "\nThe offending line appears to be:\n\n%s\n%s\n%s\n" % (prev_line.rstrip(), target_line.rstrip(), arrow_line) 

158 

159 # TODO: There may be cases where there is a valid tab in a line that has other errors. 

160 if '\t' in target_line: 

161 error_message += YAML_COMMON_LEADING_TAB_ERROR 

162 # common error/remediation checking here: 

163 # check for unquoted vars starting lines 

164 if ('{{' in target_line and '}}' in target_line) and ('"{{' not in target_line or "'{{" not in target_line): 

165 error_message += YAML_COMMON_UNQUOTED_VARIABLE_ERROR 

166 # check for common dictionary mistakes 

167 elif ":{{" in stripped_line and "}}" in stripped_line: 

168 error_message += YAML_COMMON_DICT_ERROR 

169 # check for common unquoted colon mistakes 

170 elif (len(target_line) and 

171 len(target_line) > 1 and 

172 len(target_line) > col_number and 

173 target_line[col_number] == ":" and 

174 target_line.count(':') > 1): 

175 error_message += YAML_COMMON_UNQUOTED_COLON_ERROR 

176 # otherwise, check for some common quoting mistakes 

177 else: 

178 # FIXME: This needs to split on the first ':' to account for modules like lineinfile 

179 # that may have lines that contain legitimate colons, e.g., line: 'i ALL= (ALL) NOPASSWD: ALL' 

180 # and throw off the quote matching logic. 

181 parts = target_line.split(":") 

182 if len(parts) > 1: 

183 middle = parts[1].strip() 

184 match = False 

185 unbalanced = False 

186 

187 if middle.startswith("'") and not middle.endswith("'"): 

188 match = True 

189 elif middle.startswith('"') and not middle.endswith('"'): 

190 match = True 

191 

192 if (len(middle) > 0 and 

193 middle[0] in ['"', "'"] and 

194 middle[-1] in ['"', "'"] and 

195 target_line.count("'") > 2 or 

196 target_line.count('"') > 2): 

197 unbalanced = True 

198 

199 if match: 

200 error_message += YAML_COMMON_PARTIALLY_QUOTED_LINE_ERROR 

201 if unbalanced: 

202 error_message += YAML_COMMON_UNBALANCED_QUOTES_ERROR 

203 

204 except (IOError, TypeError): 

205 error_message += '\n(could not open file to display line)' 

206 except IndexError: 

207 error_message += '\n(specified line no longer in file, maybe it changed?)' 

208 

209 return error_message 

210 

211 

212class AnsiblePromptInterrupt(AnsibleError): 

213 '''User interrupt''' 

214 

215 

216class AnsiblePromptNoninteractive(AnsibleError): 

217 '''Unable to get user input''' 

218 

219 

220class AnsibleAssertionError(AnsibleError, AssertionError): 

221 '''Invalid assertion''' 

222 pass 

223 

224 

225class AnsibleOptionsError(AnsibleError): 

226 ''' bad or incomplete options passed ''' 

227 pass 

228 

229 

230class AnsibleParserError(AnsibleError): 

231 ''' something was detected early that is wrong about a playbook or data file ''' 

232 pass 

233 

234 

235class AnsibleInternalError(AnsibleError): 

236 ''' internal safeguards tripped, something happened in the code that should never happen ''' 

237 pass 

238 

239 

240class AnsibleRuntimeError(AnsibleError): 

241 ''' ansible had a problem while running a playbook ''' 

242 pass 

243 

244 

245class AnsibleModuleError(AnsibleRuntimeError): 

246 ''' a module failed somehow ''' 

247 pass 

248 

249 

250class AnsibleConnectionFailure(AnsibleRuntimeError): 

251 ''' the transport / connection_plugin had a fatal error ''' 

252 pass 

253 

254 

255class AnsibleAuthenticationFailure(AnsibleConnectionFailure): 

256 '''invalid username/password/key''' 

257 pass 

258 

259 

260class AnsibleCallbackError(AnsibleRuntimeError): 

261 ''' a callback failure ''' 

262 pass 

263 

264 

265class AnsibleTemplateError(AnsibleRuntimeError): 

266 '''A template related error''' 

267 pass 

268 

269 

270class AnsibleFilterError(AnsibleTemplateError): 

271 ''' a templating failure ''' 

272 pass 

273 

274 

275class AnsibleLookupError(AnsibleTemplateError): 

276 ''' a lookup failure ''' 

277 pass 

278 

279 

280class AnsibleUndefinedVariable(AnsibleTemplateError): 

281 ''' a templating failure ''' 

282 pass 

283 

284 

285class AnsibleFileNotFound(AnsibleRuntimeError): 

286 ''' a file missing failure ''' 

287 

288 def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None, paths=None, file_name=None): 

289 

290 self.file_name = file_name 

291 self.paths = paths 

292 

293 if message: 

294 message += "\n" 

295 if self.file_name: 

296 message += "Could not find or access '%s'" % to_text(self.file_name) 

297 else: 

298 message += "Could not find file" 

299 

300 if self.paths and isinstance(self.paths, Sequence): 

301 searched = to_text('\n\t'.join(self.paths)) 

302 if message: 

303 message += "\n" 

304 message += "Searched in:\n\t%s" % searched 

305 

306 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" 

307 

308 super(AnsibleFileNotFound, self).__init__(message=message, obj=obj, show_content=show_content, 

309 suppress_extended_error=suppress_extended_error, orig_exc=orig_exc) 

310 

311 

312# These Exceptions are temporary, using them as flow control until we can get a better solution. 

313# DO NOT USE as they will probably be removed soon. 

314# We will port the action modules in our tree to use a context manager instead. 

315class AnsibleAction(AnsibleRuntimeError): 

316 ''' Base Exception for Action plugin flow control ''' 

317 

318 def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None, result=None): 

319 

320 super(AnsibleAction, self).__init__(message=message, obj=obj, show_content=show_content, 

321 suppress_extended_error=suppress_extended_error, orig_exc=orig_exc) 

322 if result is None: 

323 self.result = {} 

324 else: 

325 self.result = result 

326 

327 

328class AnsibleActionSkip(AnsibleAction): 

329 ''' an action runtime skip''' 

330 

331 def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None, result=None): 

332 super(AnsibleActionSkip, self).__init__(message=message, obj=obj, show_content=show_content, 

333 suppress_extended_error=suppress_extended_error, orig_exc=orig_exc, result=result) 

334 self.result.update({'skipped': True, 'msg': message}) 

335 

336 

337class AnsibleActionFail(AnsibleAction): 

338 ''' an action runtime failure''' 

339 def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None, result=None): 

340 super(AnsibleActionFail, self).__init__(message=message, obj=obj, show_content=show_content, 

341 suppress_extended_error=suppress_extended_error, orig_exc=orig_exc, result=result) 

342 self.result.update({'failed': True, 'msg': message, 'exception': traceback.format_exc()}) 

343 

344 

345class _AnsibleActionDone(AnsibleAction): 

346 ''' an action runtime early exit''' 

347 pass 

348 

349 

350class AnsiblePluginError(AnsibleError): 

351 ''' base class for Ansible plugin-related errors that do not need AnsibleError contextual data ''' 

352 def __init__(self, message=None, plugin_load_context=None): 

353 super(AnsiblePluginError, self).__init__(message) 

354 self.plugin_load_context = plugin_load_context 

355 

356 

357class AnsiblePluginRemovedError(AnsiblePluginError): 

358 ''' a requested plugin has been removed ''' 

359 pass 

360 

361 

362class AnsiblePluginCircularRedirect(AnsiblePluginError): 

363 '''a cycle was detected in plugin redirection''' 

364 pass 

365 

366 

367class AnsibleCollectionUnsupportedVersionError(AnsiblePluginError): 

368 '''a collection is not supported by this version of Ansible''' 

369 pass 

370 

371 

372class AnsibleFilterTypeError(AnsibleTemplateError, TypeError): 

373 ''' a Jinja filter templating failure due to bad type''' 

374 pass 

375 

376 

377class AnsiblePluginNotFound(AnsiblePluginError): 

378 ''' Indicates we did not find an Ansible plugin ''' 

379 pass