处理失败任务

任务执行失败是不可避免的,现在接着上一节的示例来看下如何处理失败的任务。

上一节中的示例代码:

  1. [1]:
  1. import logging
  2. from nornir import InitNornir
  3. from nornir.core.task import Task, Result
  4. from nornir_utils.plugins.functions import print_result
  5. nr = InitNornir(config_file="files/config.yaml")
  6. spine_bj = nr.filter(site="bj", role="spine")
  7. def count(task: Task, number: int) -> Result:
  8. return Result(
  9. host=task.host,
  10. result=f"{[n for n in range(0, number)]}"
  11. )
  12. def say(task: Task, text: str) -> Result:
  13. if task.host.name == "spine01.bj":
  14. raise Exception(f"{task.host.name} 不能输出信息")
  15. return Result(
  16. host=task.host,
  17. result=f"{task.host.name} says {text}"
  18. )
  19. def greet_and_count(task: Task, number: int) -> Result:
  20. task.run(
  21. name="你好~",
  22. severity_level=logging.DEBUG,
  23. task=say,
  24. text="Hi~",
  25. )
  26. task.run(
  27. name="计数",
  28. task=count,
  29. number=number,
  30. )
  31. task.run(
  32. name="再见",
  33. severity_level=logging.DEBUG,
  34. task=say,
  35. text="byebye."
  36. )
  37. # 计算打招呼打了奇数次还是偶数次
  38. even_or_odds = "even" if number % 2 == 1 else "odd"
  39. return Result(
  40. host=task.host,
  41. result = f"{task.host} counted {even_or_odds} times!",
  42. )
  43. result = spine_bj.run(
  44. task=greet_and_count,
  45. number=5
  46. )

在这段示例代码中,任务 say 针对 spine01.bj 主机抛出了一个异常,这导致整个任务的执行结果是失败的:

  1. [2]:
  1. result.failed
  1. [2]:
  1. True
  1. [3]:
  1. # 查看是哪些主机导致了失败
  2. result.failed_hosts
  1. [3]:
  1. {'spine01.bj': MultiResult: [Result: "greet_and_count", Result: "你好~"]}

如果任务发生了失败,可以通过 exception 显示异常信息:

  1. [4]:
  1. result["spine01.bj"].exception
  1. [4]:
  1. nornir.core.exceptions.NornirSubTaskError()

上一条命令显示结果是子任务错误,可以通过列表取值来查看错误信息:

  1. [5]:
  1. result["spine01.bj"][1].exception
  1. [5]:
  1. Exception('spine01.bj 不能输出信息')

想要查看更具体的信息,可以使用 print_result 查看具体的异常信息:

  1. [6]:
  1. print_result(result["spine01.bj"])
  1. vvvv spine01.bj: greet_and_count ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvv ERROR
  2. Subtask: 你好~ (failed)
  3. ---- 你好~ ** changed : False ---------------------------------------------------- ERROR
  4. Traceback (most recent call last):
  5. File "c:\program files\python38\lib\site-packages\nornir\core\task.py", line 99, in start
  6. r = self.task(self, **self.params)
  7. File "C:\Users\xdai\AppData\Local\Temp/ipykernel_35768/1441132238.py", line 18, in say
  8. raise Exception(f"{task.host.name} 不能输出信息")
  9. Exception: spine01.bj 不能输出信息
  10. ^^^^ END greet_and_count ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

在处理任务执行结果的过程中,如果有执行出错的话,还会抛出 NornirExecutionError 异常,可以使用 raise_on_error 方法来引出这个异常,然后使用 try 子句进行处理:

  1. [7]:
  1. from nornir.core.exceptions import NornirExecutionError
  2. try:
  3. result.raise_on_error()
  4. except NornirExecutionError:
  5. print("ERROR!!!")
  1. ERROR!!!

跳过失败的主机

Nornir 会跟踪记录任务执行失败的主机,然后不在该主机上运行其他新的任务。

现在定义一个新的任务,并使用之前示例筛选出来的主机组 spine_bj 来执行该任务。

这里需要注意一下: spine_bj 中有两个主机,但是之前示例中,spine01.bj 在执行任务组 greet_and_count 中失败了。

  1. [8]:
  1. spine_bj.inventory.hosts
  1. [8]:
  1. {'spine00.bj': Host: spine00.bj, 'spine01.bj': Host: spine01.bj}
  1. [9]:
  1. from nornir.core.task import Result
  2. def hi(task: Task) -> Result:
  3. return Result(
  4. host=task.host,
  5. result=f"{task.host.name}: Hi, I am still here!"
  6. )
  7. result = spine_bj.run(hi)
  1. [10]:
  1. print_result(result)
  1. hi******************************************************************************
  2. * spine00.bj ** changed : False ************************************************
  3. vvvv hi ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
  4. spine00.bj: Hi, I am still here!
  5. ^^^^ END hi ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

查看执行出来的结果,只有第一台主机 spine00.bj 成功执行了新的任务。

如果需要新任务在失败的主机上执行,需要在执行调用时添加 on_failed=True

  1. [11]:
  1. result = spine_bj.run(task=hi, on_failed=True)
  2. print_result(result)
  1. hi******************************************************************************
  2. * spine00.bj ** changed : False ************************************************
  3. vvvv hi ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
  4. spine00.bj: Hi, I am still here!
  5. ^^^^ END hi ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  6. * spine01.bj ** changed : False ************************************************
  7. vvvv hi ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
  8. spine01.bj: Hi, I am still here!
  9. ^^^^ END hi ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

如果只想在失败的主机上执行新任务,可以使用 on_good 参数: >上一个代码框中使用了 on_failed=True,导致两个主机都执行成功了。如果想要验证 on_good ,需要再执行一下之前导致错误的任务组来看到这次的结果

  1. [12]:
  1. # 这是上一节示例中执行失败的任务组,再次执行一下,来验证 `on_good`
  2. result = spine_bj.run(
  3. task=greet_and_count,
  4. number=5
  5. )
  1. [13]:
  1. result = spine_bj.run(task=hi, on_failed=True, on_good=False)
  2. print_result(result)
  1. hi******************************************************************************
  2. * spine01.bj ** changed : False ************************************************
  3. vvvv hi ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
  4. spine01.bj: Hi, I am still here!
  5. ^^^^ END hi ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

可以看到只在失败的主机上执行了新任务。

如何实现的呢?

为了实现这种效果,nornir 是通过在 data 对象中添加了 failed_hosts 字段来让任务之间共享失败的主机(有关 data 对象,可以回顾一下初始化 Nornir):

  1. [14]:
  1. nr.data.failed_hosts
  1. [14]:
  1. {'spine01.bj'}

如果要将某些主机标记为成功并让它们重新符合执行新任务的资格,可以使用函数 recovery_host 为某个主机单独执行此操作,或者使用 reset_failed_hosts 完全重置失败列表:

  1. [15]:
  1. nr.data.recover_host('spine01.bj')
  2. nr.data.failed_hosts
  1. [15]:
  1. set()
  1. [16]:
  1. nr.data.reset_failed_hosts()
  2. nr.data.failed_hosts
  1. [16]:
  1. set()

自动抛出异常

一般情况下,如果任务执行出错,只能在最终打印任务结果时看到错误信息,如果需要及时反馈或者处理失败的失误,可以在初始化 nornir 对象时添加 raise_on_error 来让任务出错时自动引发异常:

  1. [17]:
  1. nr = InitNornir(
  2. config_file="files/config.yaml",
  3. core = {"raise_on_error": True}
  4. )
  5. spine_bj = nr.filter(site='bj', role='spine')
  6. try:
  7. result = spine_bj.run(
  8. task=greet_and_count,
  9. number=5,
  10. )
  11. except NornirExecutionError:
  12. print("ERROR!!!")
  1. ERROR!!!

工作流

由任务组组成的工作流(Workflow)适用于大多数使用场景,因为它可以跳过出错的主机,并且 print_result 也提供了足够的信息来了解任务执行的结果。

对于更复杂的工作流,也可以通过 nornir 来实现,因为这个框架足够灵活,接下来就来看看强大的处理器。