归纳
核心类 | 用途 | 所在的模块路径 |
DataLoader | 用于读取yaml、json格式的文件 | ansible.parsing.dataloader |
Play | 存储执行hosts的角色信息 | ansible.playbook.play |
TaskQueueManager | ansible底层用到的任务队列 | ansible.executor.task_queue_manager |
PlaybookExecutor | 核心类执行playbook剧本 | ansible.executor.playbook_executor |
CallbackBase | 状态回调,各种成功失败的状态 | ansible.plugins.callback |
InventoryManager | 用于导入inventory文件 | ansible.inventory.manager |
VariableManager | 用于存储各类变量信息 | ansible.var.manager |
Host,Group | 操作单个主机或者主机组信息 | ansible.inventory.host |
功能:用来管理主机和主机组相关的资源设备信息
from ansible.parsing.dataloader import DataLoader from ansible.inventory.manager import InventoryManager from ansible.vars.manager import VariableManager # InventoryManager类 loader = DataLoader() # 实例化对象 inv = InventoryManager(loader=loader,sources=[‘auto_hosts‘])
# add_host()方法,添加主机到指定的主机组 inv.add_host(host=‘192.168.1.112‘,port=22,group=‘test_group3‘) # get_groups_dict()方法,查看主机组资源 inv.get_groups_dict() """ {‘all‘: [‘192.168.1.112‘, ‘192.168.1.101‘, ‘192.168.1.110‘], ‘test_group1‘: [‘192.168.1.112‘, ‘192.168.1.101‘, ‘192.168.1.110‘], ‘test_group2‘: [‘192.168.1.112‘, ‘192.168.1.101‘], ‘test_group3‘: [‘192.168.1.110‘, ‘192.168.1.112‘], ‘ungrouped‘: []} """ # get_hosts() 获取所有的主机信息,返回的为列表 inv.get_hosts # [192.168.1.112, 192.168.1.101, 192.168.1.110] # get_host() 获取指定的主机对象 inv.get_host(hostname=‘192.168.1.112‘) # 192.168.1.112
功能:进行主机变量的读取
from ansible.parsing.dataloader import DataLoader from ansible.inventory.manager import InventoryManager from ansible.vars.manager import VariableManager # InventoryManager类 loader = DataLoader() # 实例化对象 inv = InventoryManager(loader=loader,sources=[‘auto_hosts‘]) #VariableManager类 variable_manager = VariableManager(loader=loader,inventory=inv) # get_vars() # 查看变量方法 variable_manager.get_vars() """ {‘ansible_playbook_python‘: ‘/usr/bin/python3‘, ‘groups‘: {‘all‘: [‘192.168.1.112‘, ‘192.168.1.101‘, ‘192.168.1.110‘], ‘test_group1‘: [‘192.168.1.112‘, ‘192.168.1.101‘, ‘192.168.1.110‘], ‘test_group2‘: [‘192.168.1.112‘, ‘192.168.1.101‘], ‘test_group3‘: [‘192.168.1.110‘, ‘192.168.1.112‘], ‘ungrouped‘: []}, ‘omit‘: ‘__omit_place_holder__04e40184623ea65c38973233a8c804a29215f21d‘, ‘playbook_dir‘: ‘/u01/autoops/script‘} """ # get_vars() # 查询主机指定的变量信息 host = inv.get_host(hostname=‘192.168.1.110‘) variable_manager.get_vars(host=host) """ {‘ansible_playbook_python‘: ‘/usr/bin/python3‘, ‘ansible_port‘: 22, ‘ansible_ssh_pass‘: 123456, ‘ansible_ssh_user‘: ‘root‘, ‘group_names‘: [‘test_group1‘, ‘test_group3‘], ‘groups‘: {‘all‘: [‘192.168.1.112‘, ‘192.168.1.101‘, ‘192.168.1.110‘], ‘test_group1‘: [‘192.168.1.112‘, ‘192.168.1.101‘, ‘192.168.1.110‘], ‘test_group2‘: [‘192.168.1.112‘, ‘192.168.1.101‘], ‘test_group3‘: [‘192.168.1.110‘, ‘192.168.1.112‘], ‘ungrouped‘: []}, ‘inventory_dir‘: ‘/u01/autoops/script‘, ‘inventory_file‘: ‘/u01/autoops/script/auto_hosts‘, ‘inventory_hostname‘: ‘192.168.1.110‘, ‘inventory_hostname_short‘: ‘192‘, ‘omit‘: ‘__omit_place_holder__3d8bb8c134c30295abd201bee6ef34093e580c2f‘, ‘playbook_dir‘: ‘/u01/autoops/script‘} """ # set_host_variable() # 修改指定主机的变量信息 variable_manager.set_host_variable(host=host,varname=‘ansible_ssh_pass‘,value=‘111111‘) variable_manager.get_vars(host=host) """ {‘ansible_playbook_python‘: ‘/usr/bin/python3‘, ‘ansible_port‘: 22, ‘ansible_ssh_pass‘: ‘111111‘, ‘ansible_ssh_user‘: ‘root‘, ‘group_names‘: [‘test_group1‘, ‘test_group3‘], ‘groups‘: {‘all‘: [‘192.168.1.112‘, ‘192.168.1.101‘, ‘192.168.1.110‘], ‘test_group1‘: [‘192.168.1.112‘, ‘192.168.1.101‘, ‘192.168.1.110‘], ‘test_group2‘: [‘192.168.1.112‘, ‘192.168.1.101‘], ‘test_group3‘: [‘192.168.1.110‘, ‘192.168.1.112‘], ‘ungrouped‘: []}, ‘inventory_dir‘: ‘/u01/autoops/script‘, ‘inventory_file‘: ‘/u01/autoops/script/auto_hosts‘, ‘inventory_hostname‘: ‘192.168.1.110‘, ‘inventory_hostname_short‘: ‘192‘, ‘omit‘: ‘__omit_place_holder__3d8bb8c134c30295abd201bee6ef34093e580c2f‘, ‘playbook_dir‘: ‘/u01/autoops/script‘}
""" # extra_vars={} # 添加指定对象的扩展变量,全局有效 variable_manager.extra_vars={‘myweb‘:‘jd.com‘,‘myname‘:‘jacob‘} variable_manager.get_vars(host=host) """ {‘ansible_playbook_python‘: ‘/usr/bin/python3‘, ‘ansible_port‘: 22, ‘ansible_ssh_pass‘: ‘111111‘, ‘ansible_ssh_user‘: ‘root‘, ‘group_names‘: [‘test_group1‘, ‘test_group2‘, ‘test_group3‘], ‘groups‘: {‘all‘: [‘192.168.1.112‘, ‘192.168.1.101‘, ‘192.168.1.110‘], ‘test_group1‘: [‘192.168.1.112‘, ‘192.168.1.101‘, ‘192.168.1.110‘], ‘test_group2‘: [‘192.168.1.112‘, ‘192.168.1.101‘], ‘test_group3‘: [‘192.168.1.110‘, ‘192.168.1.112‘], ‘ungrouped‘: []}, ‘inventory_dir‘: ‘/u01/autoops/script‘, ‘inventory_file‘: ‘/u01/autoops/script/auto_hosts‘, ‘inventory_hostname‘: ‘192.168.1.110‘, ‘inventory_hostname_short‘: ‘192‘, ‘myname‘: ‘jacob‘, ‘myweb‘: ‘jd.com‘, ‘omit‘: ‘__omit_place_holder__04e40184623ea65c38973233a8c804a29215f21d‘, ‘playbook_dir‘: ‘/u01/autoops/script‘}
"""
ansible的ad-hoc模式的调用示例:
ad-hoc模式一般用于批量执行简单命令,文件替换等。此处的重点是执行对象和模块,资源资产配置清单,执行选项。
所需类及其调用关系为:
2.3.1 namedtuple的用法回顾
from collections import namedtuple # User = namedtuple(‘User‘, ‘name age id‘) User = namedtuple(‘User‘, [‘name‘, ‘age‘, ‘id‘]) user = User(‘tester‘, ‘22‘, ‘24242432‘) print(user) # User(name=‘tester‘, age=‘22‘, id=‘24242432‘)
2.3.2 ansible的ad-hoc模式options总结
-v, --verbose:输出更详细的执行过程信息,-vvv可得到所有执行过程信息。 -i PATH, --inventory=PATH:指定inventory信息,默认/etc/ansible/hosts。 -f NUM, --forks=NUM:并发线程数,默认5个线程。 --private-key=PRIVATE_KEY_FILE:指定密钥文件。 -m NAME, --module-name=NAME:指定执行使用的模块。 -M DIRECTORY, --module-path=DIRECTORY:指定模块存放路径,默认/usr/share/ansible,也可以通过ANSIBLE_LIBRARY设定默认路径。 -a ‘ARGUMENTS‘, --args=‘ARGUMENTS‘:模块参数。 -k, --ask-pass SSH:认证密码。 -K, --ask-sudo-pass sudo:用户的密码(—sudo时使用)。 -o, --one-line:标准输出至一行。 -s, --sudo:相当于Linux系统下的sudo命令。 -t DIRECTORY, --tree=DIRECTORY:输出信息至DIRECTORY目录下,结果文件以远程主机名命名。 -T SECONDS, --timeout=SECONDS:指定连接远程主机的最大超时,单位是:秒。 -B NUM, --background=NUM:后台执行命令,超NUM秒后kill正在执行的任务。 -P NUM, --poll=NUM:定期返回后台任务进度。 -u USERNAME, --user=USERNAME:指定远程主机以USERNAME运行命令。 -U SUDO_USERNAME, --sudo-user=SUDO_USERNAM:E使用sudo,相当于Linux下的sudo命令。 -c CONNECTION, --connection=CONNECTION:指定连接方式,可用选项paramiko (SSH), ssh, local。Local方式常用于crontab 和 kickstarts。 -l SUBSET, --limit=SUBSET:指定运行主机。 -l ~REGEX, --limit=~REGEX:指定运行主机(正则)。 --list-hosts:列出符合条件的主机列表,不执行任何其他命令
2.3.3 结合options和play类的使用的简单案例
# -*- coding: utf-8 -*- #!/usr/bin/env python #核心类 from collections import namedtuple from ansible.parsing.dataloader import DataLoader from ansible.vars.manager import VariableManager from ansible.inventory.manager import InventoryManager from ansible.playbook.play import Play from ansible.executor.task_queue_manager import TaskQueueManager from ansible.plugins.callback import CallbackBase #InventoryManager类 loader = DataLoader() inventory = InventoryManager(loader=loader,sources=[‘auto_hosts‘]) #VariableManager类 variable_manager = VariableManager(loader=loader,inventory=inventory) #Options 执行选项 Options = namedtuple(‘Options‘, [‘connection‘, ‘remote_user‘, ‘ask_sudo_pass‘, ‘verbosity‘, ‘ack_pass‘, ‘module_path‘, ‘forks‘, ‘become‘, ‘become_method‘, ‘become_user‘, ‘check‘, ‘listhosts‘, ‘listtasks‘, ‘listtags‘, ‘syntax‘, ‘sudo_user‘, ‘sudo‘, ‘diff‘]) options = Options(connection=‘smart‘, remote_user=None, ack_pass=None, sudo_user=None, forks=5, sudo=None, ask_sudo_pass=False, verbosity=5, module_path=None, become=None, become_method=None, become_user=None, check=False, diff=False, listhosts=None, listtasks=None, listtags=None, syntax=None) #Play 执行对象和模块 play_source = dict( name = "Ansible Play ad-hoc test", # 任务执行的名称 hosts = ‘192.168.102.101‘, # # 控制着任务执行的目标主机,可以通过逗号填入多台主机,或者正则匹配,或者主机组 gather_facts = ‘no‘, # 执行任务之前去获取响应主机的相关信息,建议关闭,提高执行效率 tasks = [ # 以dict的方式实现,一个任务一个dict,可以写多个,module 为对应模块,args为传入的参数 dict(action=dict(module=‘shell‘, args=‘touch /tmp/ad_hoc_test1‘)), # dict(action=dict(module=‘debug‘, args=dict(msg=‘{{shell_out.stdout}}‘))) ] ) play = Play().load(play_source, variable_manager=variable_manager, loader=loader) passwords = dict() # 没有实际作用,实际中密码已经写在文件中了 tqm = TaskQueueManager( inventory=inventory, variable_manager=variable_manager, loader=loader, options=options, passwords=passwords, ) result = tqm.run(play)
[root@test01 script]# python3 ansible_api_k1.py PLAY [Ansible Play ad-hoc test] ******************************************************************************************************************************************* TASK [command] ************************************************************************************************************************************************************ [WARNING]: Consider using file module with state=touch rather than running touch changed: [192.168.102.101] # 且在192.168.102.101生成/tmp/ad_hoc_test1文件
ansible的paybook模式的调用示例:
palybook模式一般用于批量执行复杂的自动化任务,如批量安装等。此处的重点是资源资产配置清单,执行选项等。
所需类及其调用关系为:
2.4.1 playbook结合options和play类的使用的简单案例
# -*- coding: utf-8 -*- #!/usr/bin/env python #核心类 from collections import namedtuple from ansible.parsing.dataloader import DataLoader from ansible.vars.manager import VariableManager from ansible.inventory.manager import InventoryManager from ansible.executor.playbook_executor import PlaybookExecutor from ansible.playbook.play import Play from ansible.executor.task_queue_manager import TaskQueueManager #InventoryManager类 loader = DataLoader() inventory = InventoryManager(loader=loader,sources=[‘auto_hosts‘]) #VariableManager类 variable_manager = VariableManager(loader=loader,inventory=inventory) #Options 执行选项 Options = namedtuple(‘Options‘, [‘connection‘, ‘remote_user‘, ‘ask_sudo_pass‘, ‘verbosity‘, ‘ack_pass‘, ‘module_path‘, ‘forks‘, ‘become‘, ‘become_method‘, ‘become_user‘, ‘check‘, ‘listhosts‘, ‘listtasks‘, ‘listtags‘, ‘syntax‘, ‘sudo_user‘, ‘sudo‘, ‘diff‘]) options = Options(connection=‘smart‘, remote_user=None, ack_pass=None, sudo_user=None, forks=5, sudo=None, ask_sudo_pass=False, verbosity=5, module_path=None, become=None, become_method=None, become_user=None, check=False, diff=False, listhosts=None, listtasks=None, listtags=None, syntax=None) #PlaybookExecutor 执行playbook passwords = dict() playbook = PlaybookExecutor(playbooks=[‘touch.yml‘], # 注意路径,多个基本直接添加即可 inventory=inventory, variable_manager=variable_manager, loader=loader, options=options, passwords=passwords, ) playbook.run()
--- - hosts: 192.168.102.101 remote_user: root vars: touch_file: touch.file tasks: - name: touch file shell: "touch /tmp/{{touch_file}}"
PLAY [192.168.102.101] **************************************************************************************************************************************************** TASK [Gathering Facts] **************************************************************************************************************************************************** ok: [192.168.102.101] TASK [touch file] ********************************************************************************************************************************************************* [WARNING]: Consider using file module with state=touch rather than running touch changed: [192.168.102.101] PLAY RECAP **************************************************************************************************************************************************************** 192.168.102.101 : ok=2 changed=1 unreachable=0 failed=0 # 会在192.168.102.101上生成/tmp/touch.file
通过上面脚本的执行我们会发现脚本执行结果并不是很友善,比如执行任务的时间,执行任务所耗费时间,执行了什么,特别是工程中我们将输出结果发送到前端进行展示以及后端日志保存,如果只是简单地字符串则就不易操作,此时就要使用callback的改写。
2.5.1 为什么要重写callback
为了自定义格式输出
2.5.2 怎么改写callback
1. 通过子类继承父类(callbackbase)
2. 通过子类改写父类的部分方法
v2_runner_on_unreachable
v2_runner_on_ok
v2_runner_on_failed
2.5.3 重写ad-hoc模式的类
# -*- coding: utf-8 -*- # !/usr/bin/env python # 核心类 from collections import namedtuple from ansible.parsing.dataloader import DataLoader from ansible.vars.manager import VariableManager from ansible.inventory.manager import InventoryManager from ansible.playbook.play import Play from ansible.executor.task_queue_manager import TaskQueueManager from ansible.plugins.callback import CallbackBase # InventoryManager类 loader = DataLoader() inventory = InventoryManager(loader=loader, sources=[‘auto_hosts‘]) # VariableManager类 variable_manager = VariableManager(loader=loader, inventory=inventory) # Options 执行选项 Options = namedtuple(‘Options‘, [‘connection‘, ‘remote_user‘, ‘ask_sudo_pass‘, ‘verbosity‘, ‘ack_pass‘, ‘module_path‘, ‘forks‘, ‘become‘, ‘become_method‘, ‘become_user‘, ‘check‘, ‘listhosts‘, ‘listtasks‘, ‘listtags‘, ‘syntax‘, ‘sudo_user‘, ‘sudo‘, ‘diff‘]) options = Options(connection=‘smart‘, remote_user=None, ack_pass=None, sudo_user=None, forks=5, sudo=None, ask_sudo_pass=False, verbosity=5, module_path=None, become=None, become_method=None, become_user=None, check=False, diff=False, listhosts=None, listtasks=None, listtags=None, syntax=None) # Play 执行对象和模块 play_source = dict( name="Ansible Play ad-hoc test", # 任务执行的名称 hosts=‘192.168.102.101‘, # 控制着任务执行的目标主机,可以通过逗号填入多台主机,或者正则匹配,或者主机组 gather_facts=‘no‘, # 执行任务之前去获取响应主机的相关信息,建议关闭,提高执行效率 tasks=[ # 以dict的方式实现,一个任务一个dict,可以写多个,module 为对应模块,args为传入的参数 dict(action=dict(module=‘shell‘, args=‘touch /tmp/ad_hoc_test1‘)), # dict(action=dict(module=‘debug‘, args=dict(msg=‘{{shell_out.stdout}}‘))) ] ) play = Play().load(play_source, variable_manager=variable_manager, loader=loader) class ModelResultsCollector(CallbackBase): # 继承父类CallbackBase """ 重写callbackBase类的部分方法 """ def __init__(self, *args, **kwargs): super(ModelResultsCollector, self).__init__(*args, **kwargs) # 初始化父类方法 self.host_ok = {} self.host_unreachable = {} self.host_failed = {} def v2_runner_on_unreachable(self, result): # result 为父类中获取所有执行结果信息的对象 self.host_unreachable[result._host.get_name()] = result def v2_runner_on_ok(self, result): self.host_ok[result._host.get_name()] = result def v2_runner_on_failed(self, result): self.host_failed[result._host.get_name()] = result callback = ModelResultsCollector() passwords = dict() tqm = TaskQueueManager( inventory=inventory, variable_manager=variable_manager, loader=loader, options=options, passwords=passwords, # 没有实际的作用 stdout_callback=callback, ) result = tqm.run(play) print(callback.host_ok.items()) # [(u"192.168.102.101",<ansible.executor.task_result.TaskResult object at 0x10406b790>)] result_raw = {‘success‘: {}, ‘failed‘: {}, ‘unreachable‘: {}} for host, result in callback.host_ok.items(): result_raw[‘success‘][host] = result._result # _result属性来获取任务执行的结果 for host, result in callback.host_failed.items(): result_raw[‘failed‘][host] = result._result for host, result in callback.host_unreachable.items(): result_raw[‘unreachable‘][host] = result._result print(result_raw)
{ ‘success‘: { ‘192.168.102.101‘: { ‘changed‘: True, ‘end‘: ‘2019-03-03 21:24:29.426184‘, ‘stdout‘: ‘‘, ‘cmd‘: ‘touch /tmp/ad_hoc_test1‘, ‘rc‘: 0, ‘start‘: ‘2019-03-03 21:24:29.422987‘, ‘stderr‘: ‘‘, ‘delta‘: ‘0:00:00.003197‘, ‘invocation‘: { ‘module_args‘: { ‘warn‘: True, ‘executable‘: None, ‘_uses_shell‘: True, ‘_raw_params‘: ‘touch /tmp/ad_hoc_test1‘, ‘removes‘: None, ‘creates‘: None, ‘chdir‘: None, ‘stdin‘: None } }, ‘warnings‘: [‘Consider using file module with state=touch rather than running touch‘], ‘_ansible_parsed‘: True, ‘stdout_lines‘: [], ‘stderr_lines‘: [], ‘_ansible_no_log‘: False, ‘failed‘: False } }, ‘failed‘: {}, ‘unreachable‘: {} }
2.5.4 重写playbook模式的类
# -*- coding: utf-8 -*- # !/usr/bin/env python # 核心类 from collections import namedtuple from ansible.parsing.dataloader import DataLoader from ansible.vars.manager import VariableManager from ansible.inventory.manager import InventoryManager from ansible.executor.playbook_executor import PlaybookExecutor from ansible.playbook.play import Play from ansible.executor.task_queue_manager import TaskQueueManager from ansible.plugins.callback import CallbackBase # InventoryManager类 loader = DataLoader() inventory = InventoryManager(loader=loader, sources=[‘auto_hosts‘]) # VariableManager类 variable_manager = VariableManager(loader=loader, inventory=inventory) # Options 执行选项 Options = namedtuple(‘Options‘, [‘connection‘, ‘remote_user‘, ‘ask_sudo_pass‘, ‘verbosity‘, ‘ack_pass‘, ‘module_path‘, ‘forks‘, ‘become‘, ‘become_method‘, ‘become_user‘, ‘check‘, ‘listhosts‘, ‘listtasks‘, ‘listtags‘, ‘syntax‘, ‘sudo_user‘, ‘sudo‘, ‘diff‘]) options = Options(connection=‘smart‘, remote_user=None, ack_pass=None, sudo_user=None, forks=5, sudo=None, ask_sudo_pass=False, verbosity=5, module_path=None, become=None, become_method=None, become_user=None, check=False, diff=False, listhosts=None, listtasks=None, listtags=None, syntax=None) # CallbackBase改写 class PlayBookResultsCollector(CallbackBase): CALLBACK_VERSION = 2.0 def __init__(self, *args, **kwargs): super(PlayBookResultsCollector, self).__init__(*args, **kwargs) self.task_ok = {} self.task_skipped = {} self.task_failed = {} self.task_status = {} self.task_unreachable = {} def v2_runner_on_ok(self, result, *args, **kwargs): self.task_ok[result._host.get_name()] = result def v2_runner_on_failed(self, result, *args, **kwargs): self.task_failed[result._host.get_name()] = result def v2_runner_on_unreachable(self, result): self.task_unreachable[result._host.get_name()] = result def v2_runner_on_skipped(self, result): self.task_ok[result._host.get_name()] = result def v2_playbook_on_stats(self, stats): hosts = sorted(stats.processed.keys()) for h in hosts: t = stats.summarize(h) self.task_status[h] = { "ok": t[‘ok‘], "changed": t[‘changed‘], "unreachable": t[‘unreachable‘], "skipped": t[‘skipped‘], "failed": t[‘failures‘] } callback = PlayBookResultsCollector() # PlaybookExecutor 执行playbook passwords = dict() playbook = PlaybookExecutor(playbooks=[‘touch.yml‘], # 如果有多个剧本则在列表中写入 inventory=inventory, variable_manager=variable_manager, loader=loader, options=options, passwords=passwords, ) playbook._tqm._stdout_callback = callback # 此处调用与ad-hoc模式不同 playbook.run() results_raw = {‘skipped‘: {}, ‘failed‘: {}, ‘success‘: {}, "status": {}, ‘unreachable‘: {}, "changed": {}} for host, result in callback.task_ok.items(): results_raw[‘success‘][host] = result._result # _result属性来获取任务执行的结果 for host, result in callback.task_failed.items(): results_raw[‘failed‘][host] = result._result for host, result in callback.task_unreachable.items(): results_raw[‘unreachable‘][host] = result._result print(results_raw)
{ ‘skipped‘: {}, ‘failed‘: {}, ‘success‘: { ‘192.168.102.101‘: { ‘changed‘: True, ‘end‘: ‘2019-03-03 21:49:44.851042‘, ‘stdout‘: ‘‘, ‘cmd‘: ‘touch /tmp/touch.file‘, ‘rc‘: 0, ‘start‘: ‘2019-03-03 21:49:44.848321‘, ‘stderr‘: ‘‘, ‘delta‘: ‘0:00:00.002721‘, ‘invocation‘: { ‘module_args‘: { ‘warn‘: True, ‘executable‘: None, ‘_uses_shell‘: True, ‘_raw_params‘: ‘touch /tmp/touch.file‘, ‘removes‘: None, ‘creates‘: None, ‘chdir‘: None, ‘stdin‘: None } }, ‘warnings‘: [‘Consider using file module with state=touch rather than running touch‘], ‘_ansible_parsed‘: True, ‘stdout_lines‘: [], ‘stderr_lines‘: [], ‘_ansible_no_log‘: False, ‘failed‘: False } }, ‘status‘: {}, ‘unreachable‘: {}, ‘changed‘: {} }
原文:https://www.cnblogs.com/xiao-xiong/p/10445963.html