From e57121a78018571b38e97f4ff9e9e1c1b198791f Mon Sep 17 00:00:00 2001 From: ibuler Date: Wed, 6 Dec 2017 18:31:51 +0800 Subject: [PATCH] =?UTF-8?q?[Feature]=20=E4=BC=98=E5=8C=96Ops=20ansible=20a?= =?UTF-8?q?pi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/utils.py | 6 +- apps/locale/zh/LC_MESSAGES/django.po | 12 +- apps/ops/ansible/callback.py | 99 ++++--- apps/ops/ansible/exceptions.py | 6 + apps/ops/ansible/inventory.py | 146 ++++++---- apps/ops/ansible/runner.py | 402 +++++++++++---------------- apps/ops/ansible/test_inventory.py | 63 +++++ apps/ops/ansible/test_runner.py | 54 ++++ apps/ops/api.py | 4 +- apps/ops/models.py | 43 +-- apps/ops/serializers.py | 4 +- apps/ops/tasks.py | 4 +- apps/ops/utils.py | 28 +- apps/ops/views.py | 10 +- apps/templates/_nav.html | 2 +- apps/terminal/models.py | 2 +- 16 files changed, 501 insertions(+), 384 deletions(-) create mode 100644 apps/ops/ansible/exceptions.py create mode 100644 apps/ops/ansible/test_inventory.py create mode 100644 apps/ops/ansible/test_runner.py diff --git a/apps/assets/utils.py b/apps/assets/utils.py index e67412615..644011cf1 100644 --- a/apps/assets/utils.py +++ b/apps/assets/utils.py @@ -1,9 +1,10 @@ # ~*~ coding: utf-8 ~*~ # -from ops.utils import run_AdHoc +from .models import Asset def test_admin_user_connective_manual(asset): + from ops.utils import run_AdHoc if not isinstance(asset, list): asset = [asset] task_tuple = ( @@ -15,3 +16,6 @@ def test_admin_user_connective_manual(asset): else: return True + +def get_assets_by_id_list(id_list): + return Asset.objects.filter(id__in=id_list) diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 277323bde..dfe09da5a 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -1418,23 +1418,23 @@ msgid "Assets id" msgstr "资产id" #: ops/models.py:27 -msgid "Task module and args json format" +msgid "Playbook module and args json format" msgstr "" #: ops/models.py:28 -msgid "Task run pattern" +msgid "Playbook run pattern" msgstr "" #: ops/models.py:29 -msgid "Task raw result" +msgid "Playbook raw result" msgstr "" #: ops/models.py:30 -msgid "Task summary" +msgid "Playbook summary" msgstr "" #: ops/templates/ops/task_detail.html:19 -msgid "Task replay detail" +msgid "Playbook replay detail" msgstr "任务记录详情" #: ops/templates/ops/task_detail.html:62 @@ -1669,7 +1669,7 @@ msgid "Job Center" msgstr "作业中心" #: templates/_nav.html:51 -msgid "Task" +msgid "Playbook" msgstr "任务" #: templates/_nav.html:62 diff --git a/apps/ops/ansible/callback.py b/apps/ops/ansible/callback.py index 148117991..b5dd3bbe1 100644 --- a/apps/ops/ansible/callback.py +++ b/apps/ops/ansible/callback.py @@ -1,65 +1,76 @@ # ~*~ coding: utf-8 ~*~ -from collections import defaultdict from ansible.plugins.callback import CallbackBase -class CommandResultCallback(CallbackBase): - def __init__(self, display=None): - self.result_q = dict(contacted={}, dark={}) - super(CommandResultCallback, self).__init__(display) - - def gather_result(self, n, res): - self.result_q[n][res._host.name] = {} - self.result_q[n][res._host.name]['cmd'] = res._result.get('cmd') - self.result_q[n][res._host.name]['stderr'] = res._result.get('stderr') - self.result_q[n][res._host.name]['stdout'] = res._result.get('stdout') - self.result_q[n][res._host.name]['rc'] = res._result.get('rc') - - def v2_runner_on_ok(self, result): - self.gather_result("contacted", result) - - def v2_runner_on_failed(self, result, ignore_errors=False): - self.gather_result("dark", result) - - def v2_runner_on_unreachable(self, result): - self.gather_result("dark", result) - - def v2_runner_on_skipped(self, result): - self.gather_result("dark", result) - - class AdHocResultCallback(CallbackBase): """ AdHoc result Callback """ def __init__(self, display=None): - self.result_q = dict(contacted={}, dark={}) - super(AdHocResultCallback, self).__init__(display) + # result_raw example: { + # "ok": {"hostname": []}, + # "failed": {"hostname": []}, + # "unreachable: {"hostname": []}, + # "skipped": {"hostname": []}, + # } + # results_summary example: { + # "contacted": {"hostname",...}, + # "dark": {"hostname": ["error",...],}, + # } + self.results_raw = dict(ok={}, failed={}, unreachable={}, skipped={}) + self.results_summary = dict(contacted=set(), dark={}) + super().__init__(display) - def gather_result(self, n, res): - if res._host.name in self.result_q[n]: - self.result_q[n][res._host.name].append(res._result) + def gather_result(self, t, host, res): + if self.results_raw[t].get(host): + self.results_raw[t][host].append(res) else: - self.result_q[n][res._host.name] = [res._result] + self.results_raw[t][host] = [res] + self.clean_result(t, host, res) - def v2_runner_on_ok(self, result): - self.gather_result("contacted", result) + def clean_result(self, t, host, res): + contacted = self.results_summary["contacted"] + dark = self.results_summary["dark"] + if t in ("ok", "skipped") and host not in dark: + contacted.add(host) + else: + dark[host].append(res) + if host in contacted: + contacted.remove(dark) - def v2_runner_on_failed(self, result, ignore_errors=False): - self.gather_result("dark", result) + def runner_on_ok(self, host, res): + self.gather_result("ok", host, res) - def v2_runner_on_unreachable(self, result): - self.gather_result("dark", result) + def runner_on_failed(self, host, res, ignore_errors=False): + self.gather_result("failed", host, res) - def v2_runner_on_skipped(self, result): - self.gather_result("dark", result) + def runner_on_unreachable(self, host, res): + self.gather_result("unreachable", host, res) - def v2_playbook_on_task_start(self, task, is_conditional): - pass + def runner_on_skipped(self, host, item=None): + self.gather_result("skipped", host, item) - def v2_playbook_on_play_start(self, play): - pass + +class CommandResultCallback(AdHocResultCallback): + def __init__(self, display=None): + self.results_command = dict() + super().__init__(display) + + def gather_result(self, t, host, res): + super().gather_result(t, host, res) + self.gather_cmd(t, host, res) + + def gather_cmd(self, t, host, res): + cmd = {} + if t == "ok": + cmd['cmd'] = res.get('cmd') + cmd['stderr'] = res.get('stderr') + cmd['stdout'] = res.get('stdout') + cmd['rc'] = res.get('rc') + else: + cmd['err'] = "Error: {}".format(res) + self.results_command[host] = cmd class PlaybookResultCallBack(CallbackBase): diff --git a/apps/ops/ansible/exceptions.py b/apps/ops/ansible/exceptions.py new file mode 100644 index 000000000..e49afd34a --- /dev/null +++ b/apps/ops/ansible/exceptions.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# + + +class AnsibleError(Exception): + pass diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py index 5c421e916..fb58b4ffd 100644 --- a/apps/ops/ansible/inventory.py +++ b/apps/ops/ansible/inventory.py @@ -1,31 +1,52 @@ # ~*~ coding: utf-8 ~*~ -from ansible.inventory import Inventory, Host, Group -from ansible.vars import VariableManager +from ansible.inventory.group import Group +from ansible.inventory.host import Host +from ansible.vars.manager import VariableManager +from ansible.inventory.manager import InventoryManager from ansible.parsing.dataloader import DataLoader class JMSHost(Host): - def __init__(self, asset): - self.asset = asset - self.name = name = asset.get('hostname') or asset.get('ip') - self.port = port = asset.get('port') or 22 - super(JMSHost, self).__init__(name, port) - self.set_all_variable() + def __init__(self, host_data): + """ + 初始化 + :param host_data: { + "hostname": "", + "ip": "", + "port": "", + "username": "", + "password": "", + "private_key": "", + "become": { + "method": "", + "user": "", + "pass": "", + } + "groups": [], + "vars": {}, + } + """ + self.host_data = host_data + hostname = host_data.get('hostname') or host_data.get('ip') + port = host_data.get('port') or 22 + super(JMSHost, self).__init__(hostname, port) + self.__set_required_variables() + self.__set_extra_variables() - def set_all_variable(self): - asset = self.asset - self.set_variable('ansible_host', asset['ip']) - self.set_variable('ansible_port', asset['port']) - self.set_variable('ansible_user', asset['username']) + def __set_required_variables(self): + host_data = self.host_data + self.set_variable('ansible_host', host_data['ip']) + self.set_variable('ansible_port', host_data['port']) + self.set_variable('ansible_user', host_data['username']) # 添加密码和秘钥 - if asset.get('password'): - self.set_variable('ansible_ssh_pass', asset['password']) - if asset.get('private_key'): - self.set_variable('ansible_ssh_private_key_file', asset['private_key']) + if host_data.get('password'): + self.set_variable('ansible_ssh_pass', host_data['password']) + if host_data.get('private_key'): + self.set_variable('ansible_ssh_private_key_file', host_data['private_key']) # 添加become支持 - become = asset.get("become", False) + become = host_data.get("become", False) if become: self.set_variable("ansible_become", True) self.set_variable("ansible_become_method", become.get('method', 'sudo')) @@ -34,58 +55,73 @@ class JMSHost(Host): else: self.set_variable("ansible_become", False) + def __set_extra_variables(self): + for k, v in self.host_data.get('vars', {}).items(): + self.set_variable(k, v) -class JMSInventory(Inventory): + def __repr__(self): + return self.name + + +class JMSInventory(InventoryManager): """ 提供生成Ansible inventory对象的方法 """ + loader_class = DataLoader + variable_manager_class = VariableManager + host_manager_class = JMSHost def __init__(self, host_list=None): if host_list is None: host_list = [] - assert isinstance(host_list, list) self.host_list = host_list - self.loader = DataLoader() - self.variable_manager = VariableManager() - super(JMSInventory, self).__init__(self.loader, self.variable_manager, - host_list=host_list) + assert isinstance(host_list, list) + self.loader = self.loader_class() + self.variable_manager = self.variable_manager_class() + super().__init__(self.loader) - def parse_inventory(self, host_list): - """用于生成动态构建Ansible Inventory. - self.host_list: [ - {"name": "asset_name", - "ip": , - "port": , - "user": , - "pass": , - "key": , - "groups": ['group1', 'group2'], - "other_host_var": }, - {...}, + def get_groups(self): + return self._inventory.groups + + def get_group(self, name): + return self._inventory.groups.get(name, None) + + def parse_sources(self, cache=False): + """ + 用于生成动态构建Ansible Inventory. super().__init__ 会自动调用 + host_list: [{ + "hostname": "", + "ip": "", + "port": "", + "username": "", + "password": "", + "private_key": "", + "become": { + "method": "", + "user": "", + "pass": "", + }, + "groups": [], + "vars": {}, + }, ] - :return: 返回一个Ansible的inventory对象 + :return: None """ + group_all = self.get_group('all') + ungrouped = self.get_group('ungrouped') - # TODO: 验证输入 - # 创建Ansible Group,如果没有则创建default组 - ungrouped = Group('ungrouped') - all = Group('all') - all.add_child_group(ungrouped) - self.groups = dict(all=all, ungrouped=ungrouped) - - for asset in host_list: - host = JMSHost(asset=asset) - asset_groups = asset.get('groups') - if asset_groups: - for group_name in asset_groups: - if group_name not in self.groups: + for host_data in self.host_list: + host = self.host_manager_class(host_data=host_data) + self.hosts[host_data['hostname']] = host + groups_data = host_data.get('groups') + if groups_data: + for group_name in groups_data: + group = self.get_group(group_name) + if group is None: group = Group(group_name) - self.groups[group_name] = group - else: - group = self.groups[group_name] + self.add_group(group) group.add_host(host) else: ungrouped.add_host(host) - all.add_host(host) - + group_all.add_host(host) diff --git a/apps/ops/ansible/runner.py b/apps/ops/ansible/runner.py index e82c47d73..616605114 100644 --- a/apps/ops/ansible/runner.py +++ b/apps/ops/ansible/runner.py @@ -2,303 +2,233 @@ from __future__ import unicode_literals import os -from collections import namedtuple, defaultdict +from collections import namedtuple from ansible.executor.task_queue_manager import TaskQueueManager -from ansible.vars import VariableManager +from ansible.vars.manager import VariableManager from ansible.parsing.dataloader import DataLoader from ansible.executor.playbook_executor import PlaybookExecutor from ansible.playbook.play import Play import ansible.constants as C -from ansible.utils.vars import load_extra_vars -from ansible.utils.vars import load_options_vars from .inventory import JMSInventory from .callback import AdHocResultCallback, PlaybookResultCallBack, \ CommandResultCallback from common.utils import get_logger +from .exceptions import AnsibleError __all__ = ["AdHocRunner", "PlayBookRunner"] - C.HOST_KEY_CHECKING = False - logger = get_logger(__name__) +Options = namedtuple('Options', [ + 'listtags', 'listtasks', 'listhosts', 'syntax', 'connection', + 'module_path', 'forks', 'remote_user', 'private_key_file', 'timeout', + 'ssh_common_args', 'ssh_extra_args', 'sftp_extra_args', + 'scp_extra_args', 'become', 'become_method', 'become_user', + 'verbosity', 'check', 'extra_vars', 'playbook_path', 'passwords', + 'diff', +]) + + +def get_default_options(): + options = Options( + listtags=False, + listtasks=False, + listhosts=False, + syntax=False, + timeout=60, + connection='ssh', + module_path='', + forks=10, + remote_user='root', + private_key_file=None, + ssh_common_args="", + ssh_extra_args="", + sftp_extra_args="", + scp_extra_args="", + become=None, + become_method=None, + become_user=None, + verbosity=None, + extra_vars=[], + check=False, + playbook_path='/etc/ansible/', + passwords=None, + diff=False, + ) + return options + + # Jumpserver not use playbook -class PlayBookRunner(object): +class PlayBookRunner: """ 用于执行AnsiblePlaybook的接口.简化Playbook对象的使用. """ - Options = namedtuple('Options', [ - 'listtags', 'listtasks', 'listhosts', 'syntax', 'connection', - 'module_path', 'forks', 'remote_user', 'private_key_file', 'timeout', - 'ssh_common_args', 'ssh_extra_args', 'sftp_extra_args', - 'scp_extra_args', 'become', 'become_method', 'become_user', - 'verbosity', 'check', 'extra_vars']) - def __init__(self, - hosts=None, - playbook_path=None, - forks=C.DEFAULT_FORKS, - listtags=False, - listtasks=False, - listhosts=False, - syntax=False, - module_path=None, - remote_user='root', - timeout=C.DEFAULT_TIMEOUT, - ssh_common_args=None, - ssh_extra_args=None, - sftp_extra_args=None, - scp_extra_args=None, - become=True, - become_method=None, - become_user="root", - verbosity=None, - extra_vars=None, - connection_type="ssh", - passwords=None, - private_key_file=None, - check=False): + # Default results callback + results_callback_class = PlaybookResultCallBack + inventory_class = JMSInventory + loader_class = DataLoader + variable_manager_class = VariableManager + options = get_default_options() + def __init__(self, hosts=None, options=None): + """ + :param options: Ansible options like ansible.cfg + :param hosts: [ + { + "hostname": "", + "ip": "", + "port": "", + "username": "", + "password": "", + "private_key": "", + "become": { + "method": "", + "user": "", + "pass": "", + }, + "groups": [], + "vars": {}, + }, + ] + """ + if options: + self.options = options C.RETRY_FILES_ENABLED = False - self.callbackmodule = PlaybookResultCallBack() - if playbook_path is None or not os.path.exists(playbook_path): - raise AnsibleError( - "Not Found the playbook file: %s." % playbook_path) - self.playbook_path = playbook_path - self.loader = DataLoader() - self.variable_manager = VariableManager() - self.passwords = passwords or {} - self.inventory = JMSInventory(hosts) - - self.options = self.Options( - listtags=listtags, - listtasks=listtasks, - listhosts=listhosts, - syntax=syntax, - timeout=timeout, - connection=connection_type, - module_path=module_path, - forks=forks, - remote_user=remote_user, - private_key_file=private_key_file, - ssh_common_args=ssh_common_args or "", - ssh_extra_args=ssh_extra_args or "", - sftp_extra_args=sftp_extra_args, - scp_extra_args=scp_extra_args, - become=become, - become_method=become_method, - become_user=become_user, - verbosity=verbosity, - extra_vars=extra_vars or [], - check=check + self.inventory = self.inventory_class(hosts) + self.loader = self.loader_class() + self.results_callback = self.results_callback_class() + self.playbook_path = options.playbook_path + self.variable_manager = self.variable_manager_class( + loader=self.loader, inventory=self.inventory ) + self.passwords = options.passwords + self.__check() - self.variable_manager.extra_vars = load_extra_vars(loader=self.loader, - options=self.options) - self.variable_manager.options_vars = load_options_vars(self.options) + def __check(self): + if self.options.playbook_path is None or \ + not os.path.exists(self.options.playbook_path): + raise AnsibleError( + "Not Found the playbook file: {}.".format(self.options.playbook_path) + ) + if not self.inventory.list_hosts('all'): + raise AnsibleError('Inventory is empty') - self.variable_manager.set_inventory(self.inventory) - - # 初始化playbook的executor - self.runner = PlaybookExecutor( + def run(self): + executor = PlaybookExecutor( playbooks=[self.playbook_path], inventory=self.inventory, variable_manager=self.variable_manager, loader=self.loader, options=self.options, - passwords=self.passwords) - - if self.runner._tqm: - self.runner._tqm._stdout_callback = self.callbackmodule - - def run(self): - if not self.inventory.list_hosts('all'): - raise AnsibleError('Inventory is empty') - self.runner.run() - self.runner._tqm.cleanup() - return self.callbackmodule.output - - -class AdHocRunner(object): - """ - ADHoc接口 - """ - Options = namedtuple("Options", [ - 'connection', 'module_path', 'private_key_file', "remote_user", - 'timeout', 'forks', 'become', 'become_method', 'become_user', - 'check', 'extra_vars', - ] - ) - - results_callback_class = AdHocResultCallback - - def __init__(self, - hosts=C.DEFAULT_HOST_LIST, - forks=C.DEFAULT_FORKS, # 5 - timeout=C.DEFAULT_TIMEOUT, # SSH timeout = 10s - remote_user=C.DEFAULT_REMOTE_USER, # root - module_path=None, # dirs of custome modules - connection_type="smart", - become=None, - become_method=None, - become_user=None, - check=False, - passwords=None, - extra_vars=None, - private_key_file=None, - gather_facts='no'): - - self.pattern = '' - self.variable_manager = VariableManager() - self.loader = DataLoader() - self.gather_facts = gather_facts - self.results_callback = AdHocRunner.results_callback_class() - self.options = self.Options( - connection=connection_type, - timeout=timeout, - module_path=module_path, - forks=forks, - become=become, - become_method=become_method, - become_user=become_user, - check=check, - remote_user=remote_user, - extra_vars=extra_vars or [], - private_key_file=private_key_file, + passwords=self.passwords ) - self.variable_manager.extra_vars = load_extra_vars(self.loader, - options=self.options) - self.variable_manager.options_vars = load_options_vars(self.options) - self.passwords = passwords or {} - self.inventory = JMSInventory(hosts) - self.variable_manager.set_inventory(self.inventory) - self.tasks = [] - self.play_source = None - self.play = None - self.runner = None + if executor._tqm: + executor._tqm._stdout_callback = self.results_callback + executor.run() + executor._tqm.cleanup() + return self.results_callback.output + + +class AdHocRunner: + """ + ADHoc Runner接口 + """ + results_callback_class = AdHocResultCallback + inventory_class = JMSInventory + loader_class = DataLoader + variable_manager_class = VariableManager + options = get_default_options() + default_options = get_default_options() + + def __init__(self, hosts, options=None): + if options: + self.options = options + + self.pattern = '' + self.loader = DataLoader() + self.inventory = self.inventory_class(hosts) + self.variable_manager = VariableManager(loader=self.loader, inventory=self.inventory) @staticmethod def check_module_args(module_name, module_args=''): if module_name in C.MODULE_REQUIRE_ARGS and not module_args: err = "No argument passed to '%s' module." % module_name - print(err) - return False - return True + raise AnsibleError(err) - def run(self, task_tuple, pattern='all', task_name='Ansible Ad-hoc'): - """ - :param task_tuple: (('shell', 'ls'), ('ping', '')) - :param pattern: - :param task_name: - :return: - """ - for module, args in task_tuple: - if not self.check_module_args(module, args): - return - self.tasks.append( - dict(action=dict( - module=module, - args=args, - )) + def check_pattern(self, pattern): + if not self.inventory.list_hosts("all"): + raise AnsibleError("Inventory is empty.") + + if not self.inventory.list_hosts(pattern): + raise AnsibleError( + "pattern: %s dose not match any hosts." % pattern ) - self.play_source = dict( - name=task_name, + def run(self, tasks, pattern, play_name='Ansible Ad-hoc', gather_facts='no'): + """ + :param tasks: [{'action': {'module': 'shell', 'args': 'ls'}, ...}, ] + :param pattern: all, *, or others + :param play_name: The play name + :return: + """ + results_callback = self.results_callback_class() + clean_tasks = [] + for task in tasks: + self.check_module_args(task['action']['module'], task['action'].get('args')) + clean_tasks.append(task) + + play_source = dict( + name=play_name, hosts=pattern, - gather_facts=self.gather_facts, - tasks=self.tasks + gather_facts=gather_facts, + tasks=clean_tasks ) - self.play = Play().load( - self.play_source, + play = Play().load( + play_source, variable_manager=self.variable_manager, loader=self.loader, ) - self.runner = TaskQueueManager( + tqm = TaskQueueManager( inventory=self.inventory, variable_manager=self.variable_manager, loader=self.loader, options=self.options, - passwords=self.passwords, - stdout_callback=self.results_callback, + stdout_callback=results_callback, + passwords=self.options.passwords, ) - if not self.inventory.list_hosts("all"): - raise AnsibleError("Inventory is empty.") - - if not self.inventory.list_hosts(self.pattern): - raise AnsibleError( - "pattern: %s dose not match any hosts." % self.pattern) - try: - self.runner.run(self.play) + tqm.run(play) + return results_callback except Exception as e: - logger.warning(e) - else: - logger.debug(self.results_callback.result_q) - return self.results_callback.result_q + raise AnsibleError(e) finally: - if self.runner: - self.runner.cleanup() - if self.loader: - self.loader.cleanup_all_tmp_files() - - def clean_result(self): - """ - :return: { - "success": ['hostname',], - "failed": [('hostname', 'msg'), {}], - } - """ - result = {'success': [], 'failed': []} - for host in self.results_callback.result_q['contacted']: - result['success'].append(host) - - for host, msgs in self.results_callback.result_q['dark'].items(): - msg = '\n'.join(['{} {}: {}'.format( - msg.get('module_stdout', ''), - msg.get('invocation', {}).get('module_name'), - msg.get('msg', '')) for msg in msgs]) - result['failed'].append((host, msg)) - return result + tqm.cleanup() + self.loader.cleanup_all_tmp_files() -def test_run(): - assets = [ - { - "hostname": "192.168.244.129", - "ip": "192.168.244.129", - "port": 22, - "username": "root", - "password": "redhat", - }, - ] - task_tuple = (('shell', 'ls'),) - hoc = AdHocRunner(hosts=assets) - hoc.results_callback = CommandResultCallback() - ret = hoc.run(task_tuple) - print(ret) +class CommandRunner(AdHocRunner): + results_callback_class = CommandResultCallback + modules_choices = ('shell', 'raw', 'command', 'script') - #play = PlayBookRunner(assets, playbook_path='/tmp/some.yml') - """ - # /tmp/some.yml - --- - - name: Test the plabybook API. - hosts: all - remote_user: root - gather_facts: yes - tasks: - - name: exec uptime - shell: uptime - """ - #play.run() + def execute(self, cmd, pattern, module=None): + if module and module not in self.modules_choices: + raise AnsibleError("Module should in {}".format(self.modules_choices)) + else: + module = "shell" + tasks = [ + {"action": {"module": module, "args": cmd}} + ] + hosts = self.inventory.get_hosts(pattern=pattern) + name = "Run command {} on {}".format(cmd, ", ".join([host.name for host in hosts])) + return self.run(tasks, pattern, play_name=name) -if __name__ == "__main__": - test_run() diff --git a/apps/ops/ansible/test_inventory.py b/apps/ops/ansible/test_inventory.py new file mode 100644 index 000000000..d4ff43f24 --- /dev/null +++ b/apps/ops/ansible/test_inventory.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# + +import sys +import unittest + + +sys.path.insert(0, '../..') +from ops.ansible.inventory import JMSInventory + + +class TestJMSInventory(unittest.TestCase): + def setUp(self): + host_list = [{ + "hostname": "testserver1", + "ip": "102.1.1.1", + "port": 22, + "username": "root", + "password": "password", + "private_key": "/tmp/private_key", + "become": { + "method": "sudo", + "user": "root", + "pass": None, + }, + "groups": ["group1", "group2"], + "vars": {"sexy": "yes"}, + }, { + "hostname": "testserver2", + "ip": "8.8.8.8", + "port": 2222, + "username": "root", + "password": "password", + "private_key": "/tmp/private_key", + "become": { + "method": "su", + "user": "root", + "pass": "123", + }, + "groups": ["group3", "group4"], + "vars": {"love": "yes"}, + }] + + self.inventory = JMSInventory(host_list=host_list) + + def test_hosts(self): + print("#"*10 + "Hosts" + "#"*10) + for host in self.inventory.hosts: + print(host) + + def test_groups(self): + print("#" * 10 + "Groups" + "#" * 10) + for group in self.inventory.groups: + print(group) + + def test_group_all(self): + print("#" * 10 + "all group hosts" + "#" * 10) + group = self.inventory.get_group('all') + print(group.hosts) + + +if __name__ == '__main__': + unittest.main() diff --git a/apps/ops/ansible/test_runner.py b/apps/ops/ansible/test_runner.py new file mode 100644 index 000000000..3e0426be2 --- /dev/null +++ b/apps/ops/ansible/test_runner.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# + +import unittest +import sys + +sys.path.insert(0, "../..") + +from ops.ansible.runner import AdHocRunner, CommandRunner + + +class TestAdHocRunner(unittest.TestCase): + def setUp(self): + host_data = [ + { + "hostname": "testserver", + "ip": "192.168.244.168", + "port": 22, + "username": "root", + "password": "redhat", + }, + ] + self.runner = AdHocRunner(hosts=host_data) + + def test_run(self): + tasks = [ + {"action": {"module": "shell", "args": "ls"}}, + {"action": {"module": "shell", "args": "whoami"}}, + ] + ret = self.runner.run(tasks, "all") + print(ret.results_summary) + print(ret.results_raw) + + +class TestCommandRunner(unittest.TestCase): + def setUp(self): + host_data = [ + { + "hostname": "testserver", + "ip": "192.168.244.168", + "port": 22, + "username": "root", + "password": "redhat", + }, + ] + self.runner = CommandRunner(hosts=host_data) + + def test_execute(self): + res = self.runner.execute('ls', 'all') + print(res.results_command) + + +if __name__ == "__main__": + unittest.main() diff --git a/apps/ops/api.py b/apps/ops/api.py index 3a9e45b25..878c29cdb 100644 --- a/apps/ops/api.py +++ b/apps/ops/api.py @@ -4,12 +4,12 @@ from rest_framework import viewsets from .hands import IsSuperUser -from .models import Task +from .models import Playbook from .serializers import TaskSerializer class TaskViewSet(viewsets.ModelViewSet): - queryset = Task.objects.all() + queryset = Playbook.objects.all() serializer_class = TaskSerializer permission_classes = (IsSuperUser,) diff --git a/apps/ops/models.py b/apps/ops/models.py index 7f18d7366..2feabe731 100644 --- a/apps/ops/models.py +++ b/apps/ops/models.py @@ -7,40 +7,32 @@ import uuid from django.db import models from django.utils.translation import ugettext_lazy as _ -from assets.models import Asset -__all__ = ["Task"] +__all__ = ["Playbook"] logger = logging.getLogger(__name__) -class Task(models.Model): +class AdHoc(models.Model): uuid = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, blank=True, verbose_name=_('Name')) - date_start = models.DateTimeField(auto_now_add=True, verbose_name=_('Start time')) - date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('End time')) - timedelta = models.FloatField(default=0.0, verbose_name=_('Time'), null=True) - is_finished = models.BooleanField(default=False, verbose_name=_('Is finished')) - is_success = models.BooleanField(default=False, verbose_name=_('Is success')) - assets = models.TextField(blank=True, null=True, verbose_name=_('Assets id')) # Asset inventory may be change - _modules_args = models.TextField(blank=True, null=True, verbose_name=_('Task module and args json format')) - pattern = models.CharField(max_length=64, default='all', verbose_name=_('Task run pattern')) - result = models.TextField(blank=True, null=True, verbose_name=_('Task raw result')) - summary = models.TextField(blank=True, null=True, verbose_name=_('Task summary')) + tasks = models.TextField(verbose_name=_('Tasks')) # [{'name': 'task_name', 'module': '', 'args': ''}, ] + hosts = models.TextField(blank=True, null=True, verbose_name=_('Hosts')) # Asset inventory may be change + pattern = models.CharField(max_length=64, default='all', verbose_name=_('Playbook run pattern')) - def __unicode__(self): - return "%s" % self.uuid + def __str__(self): + return "%s" % self.name - @property - def total_assets(self): - assets_id = [i for i in self.assets.split(',') if i.isdigit()] - assets = Asset.objects.filter(id__in=assets_id) + def get_hosts_mapped_assets(self): + from assets.utils import get_assets_by_id_list + assets_id = [i for i in self.hosts.split(',')] + assets = get_assets_by_id_list(assets_id) return assets @property - def assets_json(self): - return [asset._to_secret_json() for asset in self.total_assets] + def inventory(self): + return [asset._to_secret_json() for asset in self.get_hosts_mapped_assets()] @property def module_args(self): @@ -57,3 +49,12 @@ class Task(models.Model): self._modules_args = json.dumps(module_args_) +class History(models.Model): + uuid = models.UUIDField(default=uuid.uuid4, primary_key=True) + date_start = models.DateTimeField(auto_now_add=True, verbose_name=_('Start time')) + date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('End time')) + timedelta = models.FloatField(default=0.0, verbose_name=_('Time'), null=True) + is_finished = models.BooleanField(default=False, verbose_name=_('Is finished')) + is_success = models.BooleanField(default=False, verbose_name=_('Is success')) + result = models.TextField(blank=True, null=True, verbose_name=_('Playbook raw result')) + summary = models.TextField(blank=True, null=True, verbose_name=_('Playbook summary')) diff --git a/apps/ops/serializers.py b/apps/ops/serializers.py index 986ee66e6..21ba11c83 100644 --- a/apps/ops/serializers.py +++ b/apps/ops/serializers.py @@ -2,12 +2,12 @@ from __future__ import unicode_literals from rest_framework import serializers -from .models import Task +from .models import Playbook class TaskSerializer(serializers.ModelSerializer): class Meta: - model = Task + model = Playbook fields = '__all__' diff --git a/apps/ops/tasks.py b/apps/ops/tasks.py index 0718b18ca..669b3741e 100644 --- a/apps/ops/tasks.py +++ b/apps/ops/tasks.py @@ -12,8 +12,8 @@ logger = get_logger(__file__) @shared_task def rerun_task(task_id): - from .models import Task - record = Task.objects.get(uuid=task_id) + from .models import Playbook + record = Playbook.objects.get(uuid=task_id) assets = record.assets_json task_tuple = record.module_args pattern = record.pattern diff --git a/apps/ops/utils.py b/apps/ops/utils.py index b27c81671..b479440cc 100644 --- a/apps/ops/utils.py +++ b/apps/ops/utils.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, unicode_literals import json +import re import time import uuid @@ -41,16 +42,16 @@ def run_AdHoc(task_tuple, assets, runner = AdHocRunner(assets) if record: - from .models import Task - if not Task.objects.filter(uuid=task_id): - record = Task(uuid=task_id, - name=task_name, - assets=','.join(str(asset['id']) for asset in assets), - module_args=task_tuple, - pattern=pattern) + from .models import Playbook + if not Playbook.objects.filter(uuid=task_id): + record = Playbook(uuid=task_id, + name=task_name, + assets=','.join(str(asset['id']) for asset in assets), + module_args=task_tuple, + pattern=pattern) record.save() else: - record = Task.objects.get(uuid=task_id) + record = Playbook.objects.get(uuid=task_id) record.date_start = timezone.now() record.date_finished = None record.timedelta = None @@ -76,3 +77,14 @@ def run_AdHoc(task_tuple, assets, record.is_success = False record.save() return summary, result + + +UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}') + + +def is_uuid(s): + if UUID_PATTERN.match(s): + return True + else: + return False + diff --git a/apps/ops/views.py b/apps/ops/views.py index 89d7e7b05..06327e228 100644 --- a/apps/ops/views.py +++ b/apps/ops/views.py @@ -9,13 +9,13 @@ from django.views.generic import ListView, DetailView, View from django.utils import timezone from django.shortcuts import redirect, reverse -from .models import Task +from .models import Playbook from ops.tasks import rerun_task class TaskListView(ListView): paginate_by = settings.CONFIG.DISPLAY_PER_PAGE - model = Task + model = Playbook ordering = ('-date_start',) context_object_name = 'task_list' template_name = 'ops/task_list.html' @@ -53,7 +53,7 @@ class TaskListView(ListView): def get_context_data(self, **kwargs): context = { 'app': 'Ops', - 'action': 'Task record list', + 'action': 'Playbook record list', 'date_from': self.date_from_s, 'date_to': self.date_to_s, 'keyword': self.keyword, @@ -63,13 +63,13 @@ class TaskListView(ListView): class TaskDetailView(DetailView): - model = Task + model = Playbook template_name = 'ops/task_detail.html' def get_context_data(self, **kwargs): context = { 'app': 'Ops', - 'action': 'Task record detail', + 'action': 'Playbook record detail', 'results': json.loads(self.object.summary or '{}'), } kwargs.update(context) diff --git a/apps/templates/_nav.html b/apps/templates/_nav.html index d353be5c1..770911a82 100644 --- a/apps/templates/_nav.html +++ b/apps/templates/_nav.html @@ -52,7 +52,7 @@ {% trans 'Job Center' %} diff --git a/apps/terminal/models.py b/apps/terminal/models.py index c4e80da78..90f14e486 100644 --- a/apps/terminal/models.py +++ b/apps/terminal/models.py @@ -113,7 +113,7 @@ class Task(models.Model): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, choices=NAME_CHOICES, verbose_name=_("Name")) - args = models.CharField(max_length=1024, verbose_name=_("Task Args")) + args = models.CharField(max_length=1024, verbose_name=_("Playbook Args")) terminal = models.ForeignKey(Terminal, null=True, on_delete=models.CASCADE) is_finished = models.BooleanField(default=False) date_created = models.DateTimeField(auto_now_add=True)