Ansible 的强大之处在于其丰富的插件系统。插件可以帮助我们扩展 Ansible 的功能,比如我们可以编写一个插件来实现一个自定义的模块,或者一个自定义的模块来实现一个自定义的功能。
同样的如果我们也想要来实现一个插件系统的话,应该怎么做呢?我们可以将不同的任务封装为独立的模块,每个模块执行特定的操作,并提供标准接口。
首先,定义一个标准接口,以便所有模块都能够遵循该接口。每个模块都应该实现这个接口,以保证一致性。
from abc import ABC, abstractmethod class BaseModule(ABC): @abstractmethod def execute(self, host, *args, **kwargs): pass
abc
模块是 Python 标准库中的一个模块,用于定义抽象基类(Abstract Base Class,ABC)。抽象基类是一种特殊的类,它不能直接实例化,而是用于定义一组接口和方法,供其他类继承和实现。
例如,实现一个软件安装模块和一个配置文件模块,那么就可以这样实现:
class InstallSoftwareModule(BaseModule): def execute(self, host, software_name): command = f"sudo apt-get install -y {software_name}" output, error = host.execute_command(command) return output, error class ConfigureFileModule(BaseModule): def execute(self, host, config_file_path, config_content): command = f"echo '{config_content}' > {config_file_path}" output, error = host.execute_command(command) return output, error
现在我们需要的是实现一个插件系统来动态加载和执行不同的插件,插件系统可以让你在运行时加载新的模块,而不需要重新编译或修改主程序。
我们可以这样定义一个插件目录结构,每个 python 文件就是一个插件:
plugins/ ├── install_software_module.py ├── configure_file_module.py
然后可以使用 importlib
模块来动态加载插件,这里我们需要在执行引擎中增加一个方法来加载插件,代码如下:
from typing import Any import os import importlib from concurrent.futures import ThreadPoolExecutor, Future from graphlib import TopologicalSorter from host import Host from task import Task from plugin import BaseModule class ExecutionEngine: """任务执行引擎""" def __init__(self): self.hosts: list[Host] = [] self.tasks: dict[str, Task] = {} self.plugins: dict[str, BaseModule] = {} # 插件列表 def load_plugins(self, plugin_dir: str): """加载插件""" for filename in os.listdir(plugin_dir): if filename.endswith(".py") and filename != "__init__.py": module_name = filename[:-3] module_path = f"{plugin_dir}.{module_name}" module = importlib.import_module(module_path) for attr in dir(module): if isinstance(getattr(module, attr), type) and issubclass(getattr(module, attr), BaseModule): self.plugins[attr] = getattr(module, attr) def _execute_task(self, host: Host, task: Task): """执行单个任务""" if task.module != "": # 如果任务配置了模块 module_class = self.plugins.get(task.module) if module_class: module = module_class() # 创建模块实例 task.result = module.execute(host, *task.args) # 执行模块 task.status = "success" if task.result["exit_status"] == 0 else "failed" # 设置任务状态 else: task.result = host.execute_command(task.command) # 执行任务 task.status = "success" if task.result[ "exit_status"] == 0 else "failed" # 设置任务状态 return host, task # 其他方法省略
上面我们在 ExecutionEngine
类中增加了一个 load_plugins
方法,用于加载插件,然后我们在 _execute_task
方法中根据任务的模块来执行不同的模块,当然我们就需要修改下 Task
类,增加一个 module
字段,用于表示任务的模块,代码如下:
from typing import Any class Task: def __init__(self, name: str, command: str, module: str, args: list[Any], depends_on: list[str] = []): self.name = name self.command = command self.module = module self.args = args self.result: dict[str, str | int] = {} # 任务执行结果 self.status = "pending" # 任务状态,pending, success, failed.... self.depends_on = depends_on # 依赖的任务
这里我们在 Task
类中,增加了 module
和 args
字段,分别表示任务的模块和参数。那我们在 main.py
入口文件中就需要加载插件了:
from config import load_configuration from engine import ExecutionEngine from host import Host from task import Task if __name__ == '__main__': conf = load_configuration('playbook.yaml') engine = ExecutionEngine() engine.load_plugins('plugins') # 加载插件 # 添加主机到执行引擎 for host_conf in conf['hosts']: engine.add_host(Host( address=host_conf['address'], username=host_conf['username'], password=host_conf['password'] )) # 添加任务到执行引擎 for task_conf in conf['tasks']: engine.add_task( Task(name=task_conf['name'], command=task_conf['command'] if 'command' in task_conf else '', module=task_conf['module'] if 'module' in task_conf else '', args=task_conf['args'] if 'args' in task_conf else [], depends_on=task_conf.get('depends_on', []))) # 运行任务执行引擎 engine.run() print('All tasks have been executed.')
上面代码中在实例化执行引擎后,我们调用了 load_plugins
方法来加载插件,然后在添加任务的时候记得加上新添加的 module
和 args
字段,这样在执行任务时就会使用我们新添加的模块了。
现在我们就可以在 plugins
目录下创建不同的插件来实现不同的功能了,比如我们添加一个配置文件的模块,在 plugins
目录下创建一个 configure_file_module.py
文件,代码如下:
from plugin import BaseModule class ConfigureFileModule(BaseModule): def execute(self, host, config_file_path, config_content): command = f"echo '{config_content}' > {config_file_path}" return host.execute_command(command)
这样我们就实现了一个配置文件的模块,然后我们就可以在 playbook.yaml
文件中使用这个模块了,代码如下:
hosts: - address: master username: root password: "xxx" port: 22 # ssh default port - address: node1 username: root password: "xxxx" port: 22 tasks: - name: Configure file module: ConfigureFileModule args: - /etc/nginx/nginx.conf - Hello, World!
现在我们就可以运行这个脚本了,看看效果如何:
$ python3 main.py PLAY [master] *********************************************************** TASK [Configure nginx] ************************************************** ok: [master] PLAY RECAP ************************************************************** master : ok=1 failed=0 skipped=0 rescued=0 ignored=0 PLAY [node1] ************************************************************ TASK [Configure nginx] ************************************************** ok: [node1] PLAY RECAP ************************************************************** node1 : ok=1 failed=0 skipped=0 rescued=0 ignored=0 All tasks have been executed.
从上面的输出结果可以看到,我们已经成功地调用了上面的插件来执行任务,我们也可以去 master1
或者 node1
主机上查看下文件是否已经配置成功:
$ cat /etc/nginx/nginx.conf Hello, World!
可以看到文件已经配置成功了。
通过插件系统,我们可以很方便地扩展我们系统的功能,根据不同的需求来选择不同的插件,从而实现不同的功能,这也大大提高了我们系统的灵活性和可扩展性。