From 92f7209997caf669a1b847e16a955f43f0e48d50 Mon Sep 17 00:00:00 2001 From: ibuler Date: Wed, 20 Nov 2024 19:12:28 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=20playbook=20manager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../automations/check_account/manager.py | 4 +- .../automations/gather_account/manager.py | 6 + .../accounts/check_account_report.html | 2 +- .../accounts/gather_account_report.html | 63 ++++- apps/assets/automations/base/manager.py | 226 +++++++++++------- apps/assets/automations/methods.py | 2 + 6 files changed, 197 insertions(+), 106 deletions(-) diff --git a/apps/accounts/automations/check_account/manager.py b/apps/accounts/automations/check_account/manager.py index 29be1fea0..d4aeb75f0 100644 --- a/apps/accounts/automations/check_account/manager.py +++ b/apps/accounts/automations/check_account/manager.py @@ -38,8 +38,8 @@ def is_weak_password(password): @bulk_create_decorator(AccountRisk) -def create_risk(account, risk): - pass +def create_risk(data): + return AccountRisk(**data) @bulk_update_decorator(AccountRisk, update_fields=["details"]) diff --git a/apps/accounts/automations/gather_account/manager.py b/apps/accounts/automations/gather_account/manager.py index 961de24ff..cc78d3bc1 100644 --- a/apps/accounts/automations/gather_account/manager.py +++ b/apps/accounts/automations/gather_account/manager.py @@ -183,9 +183,15 @@ class GatherAccountsManager(AccountBasePlaybookManager): print("Runner failed: ", e) raise e + def on_host_error(self, host, error, result): + print(f'\033[31m {host} error: {error} \033[0m\n') + self.summary['error_assets'] += 1 + def on_host_success(self, host, result): info = self._get_nested_info(result, 'debug', 'res', 'info') asset = self.host_asset_mapper.get(host) + self.summary['success_assets'] += 1 + if asset and info: self._collect_asset_account_info(asset, info) else: diff --git a/apps/accounts/templates/accounts/check_account_report.html b/apps/accounts/templates/accounts/check_account_report.html index 820cecd97..bf2e99c10 100644 --- a/apps/accounts/templates/accounts/check_account_report.html +++ b/apps/accounts/templates/accounts/check_account_report.html @@ -66,7 +66,7 @@ {{ forloop.counter }} {{ account.asset }} {{ account.username }} - {% trans 'Week password' %} + {% trans 'Week password' %} {% endfor %} diff --git a/apps/accounts/templates/accounts/gather_account_report.html b/apps/accounts/templates/accounts/gather_account_report.html index 820cecd97..cba9c5bda 100644 --- a/apps/accounts/templates/accounts/gather_account_report.html +++ b/apps/accounts/templates/accounts/gather_account_report.html @@ -25,32 +25,69 @@ {% trans 'Time using' %}: {{ execution.duration }}s + {% trans 'Assets count' %}: {{ summary.assets }} - {% trans 'Account count' %}: - {{ summary.accounts }} + {% trans 'Asset success count' %}: + {{ summary.success_assets }} - {% trans 'Week password count' %}: - {{ summary.weak_password }} + {% trans 'Asset failed count' %}: + {{ summary.fail_assets }} - {% trans 'Ok count' %}: - {{ summary.ok }} + {% trans 'Asset not support count' %}: + {{ summary.na_assets }} + + + + {% trans 'Account new found count' %}: + {{ summary.new_accounts }} + + + + {% trans 'Sudo changed count' %}: + {{ summary.sudo_changed }} - {% trans 'No password count' %}: - {{ summary.no_secret }} + {% trans 'Groups changed count' %}: + {{ summary.groups_changed }} + + + {% trans 'Authorized key changed count' %}: + {{ summary.authorized_key_changed }}
-

{% trans 'Account check details' %}:

+

{% trans 'New found accounts' %}:

+ + + + + + + + + + {% for account in result.new_accounts %} + + + + + + {% endfor %} + +
{% trans 'No.' %}{% trans 'Asset' %}{% trans 'Username' %}
{{ forloop.counter }}{{ account.asset }}{{ account.username }}
+
+ +
+

{% trans 'New found risk' %}:

@@ -61,12 +98,12 @@ - {% for account in result.weak_password %} + {% for risk in result.risks %} - - - + + + {% endfor %} diff --git a/apps/assets/automations/base/manager.py b/apps/assets/automations/base/manager.py index 58a9604cc..5ae5333f9 100644 --- a/apps/assets/automations/base/manager.py +++ b/apps/assets/automations/base/manager.py @@ -30,46 +30,49 @@ class SSHTunnelManager: @staticmethod def file_to_json(path): - with open(path, 'r') as f: + with open(path, "r") as f: d = json.load(f) return d @staticmethod def json_to_file(path, data): - with open(path, 'w') as f: + with open(path, "w") as f: json.dump(data, f, indent=4, sort_keys=True) def local_gateway_prepare(self, runner): info = self.file_to_json(runner.inventory) servers, not_valid = [], [] - for k, host in info['all']['hosts'].items(): - jms_asset, jms_gateway = host.get('jms_asset'), host.get('jms_gateway') + for k, host in info["all"]["hosts"].items(): + jms_asset, jms_gateway = host.get("jms_asset"), host.get("jms_gateway") if not jms_gateway: continue try: server = SSHTunnelForwarder( - (jms_gateway['address'], jms_gateway['port']), - ssh_username=jms_gateway['username'], - ssh_password=jms_gateway['secret'], - ssh_pkey=jms_gateway['private_key_path'], - remote_bind_address=(jms_asset['address'], jms_asset['port']) + (jms_gateway["address"], jms_gateway["port"]), + ssh_username=jms_gateway["username"], + ssh_password=jms_gateway["secret"], + ssh_pkey=jms_gateway["private_key_path"], + remote_bind_address=(jms_asset["address"], jms_asset["port"]), ) server.start() except Exception as e: - err_msg = 'Gateway is not active: %s' % jms_asset.get('name', '') - print(f'\033[31m {err_msg} 原因: {e} \033[0m\n') + err_msg = "Gateway is not active: %s" % jms_asset.get("name", "") + print(f"\033[31m {err_msg} 原因: {e} \033[0m\n") not_valid.append(k) else: local_bind_port = server.local_bind_port - host['ansible_host'] = jms_asset['address'] = host[ - 'login_host'] = interface.get_gateway_proxy_host() - host['ansible_port'] = jms_asset['port'] = host['login_port'] = local_bind_port + host["ansible_host"] = jms_asset["address"] = host["login_host"] = ( + interface.get_gateway_proxy_host() + ) + host["ansible_port"] = jms_asset["port"] = host["login_port"] = ( + local_bind_port + ) servers.append(server) # 网域不可连接的,就不继续执行此资源的后续任务了 for a in set(not_valid): - info['all']['hosts'].pop(a) + info["all"]["hosts"].pop(a) self.json_to_file(runner.inventory, info) self.gateway_servers[runner.id] = servers @@ -100,7 +103,7 @@ class BaseManager: def pre_run(self): self.execution.date_start = timezone.now() - self.execution.save(update_fields=['date_start']) + self.execution.save(update_fields=["date_start"]) def update_execution(self): self.duration = int(time.time() - self.time_start) @@ -108,7 +111,7 @@ class BaseManager: self.execution.duration = self.duration self.execution.summary = self.summary self.execution.result = self.result - self.execution.status = 'success' + self.execution.status = "success" with safe_db_connection(): self.execution.save() @@ -120,13 +123,13 @@ class BaseManager: raise NotImplementedError def get_report_subject(self): - return f'Automation {self.execution.id} finished' + return f"Automation {self.execution.id} finished" def get_report_context(self): return { - 'execution': self.execution, - 'summary': self.execution.summary, - 'result': self.execution.result + "execution": self.execution, + "summary": self.execution.summary, + "result": self.execution.result, } def send_report_if_need(self): @@ -164,30 +167,44 @@ class BaseManager: return json.dumps(data, indent=4, sort_keys=True) +class PlaybookUtil: + bulk_size = 100 + ansible_account_policy = "privileged_first" + ansible_account_prefer = "root,Administrator" + + def __init__(self, assets, playbook_dir, inventory_path): + self.assets = assets + self.playbook_dir = playbook_dir + self.inventory_path = inventory_path + + + + class BasePlaybookManager(BaseManager): bulk_size = 100 - ansible_account_policy = 'privileged_first' - ansible_account_prefer = 'root,Administrator' + ansible_account_policy = "privileged_first" + ansible_account_prefer = "root,Administrator" def __init__(self, execution): super().__init__(execution) + # example: {'gather_fact_windows': {'id': 'gather_fact_windows', 'name': '', 'method': 'gather_fact', ...} } self.method_id_meta_mapper = { - method['id']: method + method["id"]: method for method in self.platform_automation_methods - if method['method'] == self.__class__.method_type() + if method["method"] == self.__class__.method_type() } # 根据执行方式就行分组, 不同资产的改密、推送等操作可能会使用不同的执行方式 # 然后根据执行方式分组, 再根据 bulk_size 分组, 生成不同的 playbook self.playbooks = [] - params = self.execution.snapshot.get('params') + params = self.execution.snapshot.get("params") self.params = params or {} def get_params(self, automation, method_type): - method_attr = '{}_method'.format(method_type) - method_params = '{}_params'.format(method_type) + method_attr = "{}_method".format(method_type) + method_params = "{}_params".format(method_type) method_id = getattr(automation, method_attr) automation_params = getattr(automation, method_params) - serializer = self.method_id_meta_mapper[method_id]['params_serializer'] + serializer = self.method_id_meta_mapper[method_id]["params_serializer"] if serializer is None: return {} @@ -211,11 +228,14 @@ class BasePlaybookManager(BaseManager): def prepare_runtime_dir(self): ansible_dir = settings.ANSIBLE_DIR - task_name = self.execution.snapshot['name'] - dir_name = '{}_{}'.format(task_name.replace(' ', '_'), self.execution.id) + task_name = self.execution.snapshot["name"] + dir_name = "{}_{}".format(task_name.replace(" ", "_"), self.execution.id) path = os.path.join( - ansible_dir, 'automations', self.execution.snapshot['type'], - dir_name, timezone.now().strftime('%Y%m%d_%H%M%S') + ansible_dir, + "automations", + self.execution.snapshot["type"], + dir_name, + timezone.now().strftime("%Y%m%d_%H%M%S"), ) if not os.path.exists(path): os.makedirs(path, exist_ok=True, mode=0o755) @@ -225,13 +245,13 @@ class BasePlaybookManager(BaseManager): def runtime_dir(self): path = self.prepare_runtime_dir() if settings.DEBUG_DEV: - msg = 'Ansible runtime dir: {}'.format(path) + msg = "Ansible runtime dir: {}".format(path) print(msg) return path @staticmethod def write_cert_to_file(filename, content): - with open(filename, 'w') as f: + with open(filename, "w") as f: f.write(content) return filename @@ -239,41 +259,28 @@ class BasePlaybookManager(BaseManager): if not path_dir: return host - specific = host.get('jms_asset', {}).get('secret_info', {}) - cert_fields = ('ca_cert', 'client_key', 'client_cert') + specific = host.get("jms_asset", {}).get("secret_info", {}) + cert_fields = ("ca_cert", "client_key", "client_cert") filtered = list(filter(lambda x: specific.get(x), cert_fields)) if not filtered: return host - cert_dir = os.path.join(path_dir, 'certs') + cert_dir = os.path.join(path_dir, "certs") if not os.path.exists(cert_dir): os.makedirs(cert_dir, 0o700, True) for f in filtered: - result = self.write_cert_to_file( - os.path.join(cert_dir, f), specific.get(f) - ) - host['jms_asset']['secret_info'][f] = result + result = self.write_cert_to_file(os.path.join(cert_dir, f), specific.get(f)) + host["jms_asset"]["secret_info"][f] = result return host + def on_host_method_not_enabled(self, host, **kwargs): + host["error"] = _("{} disabled".format(self.__class__.method_type())) + def host_callback(self, host, automation=None, **kwargs): method_type = self.__class__.method_type() - enabled_attr = '{}_enabled'.format(method_type) - method_attr = '{}_method'.format(method_type) - - method_enabled = ( - automation - and getattr(automation, enabled_attr) - and getattr(automation, method_attr) - and getattr(automation, method_attr) in self.method_id_meta_mapper - ) - - if not method_enabled: - host['error'] = _('{} disabled'.format(self.__class__.method_type())) - return host - - host = self.convert_cert_to_file(host, kwargs.get('path_dir')) - host['params'] = self.get_params(automation, method_type) + host = self.convert_cert_to_file(host, kwargs.get("path_dir")) + host["params"] = self.get_params(automation, method_type) return host @staticmethod @@ -282,16 +289,16 @@ class BasePlaybookManager(BaseManager): @staticmethod def generate_private_key_path(secret, path_dir): - key_name = '.' + hashlib.md5(secret.encode('utf-8')).hexdigest() + key_name = "." + hashlib.md5(secret.encode("utf-8")).hexdigest() key_path = os.path.join(path_dir, key_name) if not os.path.exists(key_path): # https://github.com/ansible/ansible-runner/issues/544 # ssh requires OpenSSH format keys to have a full ending newline. # It does not require this for old-style PEM keys. - with open(key_path, 'w') as f: + with open(key_path, "w") as f: f.write(secret) - if is_openssh_format_key(secret.encode('utf-8')): + if is_openssh_format_key(secret.encode("utf-8")): f.write("\n") os.chmod(key_path, 0o400) return key_path @@ -309,50 +316,87 @@ class BasePlaybookManager(BaseManager): @staticmethod def generate_playbook(method, sub_playbook_dir): - method_playbook_dir_path = method['dir'] - sub_playbook_path = os.path.join(sub_playbook_dir, 'project', 'main.yml') + method_playbook_dir_path = method["dir"] + sub_playbook_path = os.path.join(sub_playbook_dir, "project", "main.yml") shutil.copytree(method_playbook_dir_path, os.path.dirname(sub_playbook_path)) - with open(sub_playbook_path, 'r') as f: + with open(sub_playbook_path, "r") as f: plays = yaml.safe_load(f) for play in plays: - play['hosts'] = 'all' + play["hosts"] = "all" - with open(sub_playbook_path, 'w') as f: + with open(sub_playbook_path, "w") as f: yaml.safe_dump(plays, f) return sub_playbook_path + def on_assets_not_ansible_enabled(self, assets): + for asset in assets: + print("\t{}".format(asset)) + + def on_assets_not_method_enabled(self, assets, method_id): + for asset in assets: + print("\t{}".format(asset)) + + def on_playbook_not_found(self, assets): + pass + + def check_automation_enabled(self, platform, assets): + if not platform.automation or not platform.automation.ansible_enabled: + print(_(" - Platform {} ansible disabled").format(platform.name)) + self.on_assets_not_ansible_enabled(assets) + + automation = platform.automation + + method_type = self.__class__.method_type() + enabled_attr = "{}_enabled".format(method_type) + method_attr = "{}_method".format(method_type) + + method_enabled = ( + automation + and getattr(automation, enabled_attr) + and getattr(automation, method_attr) + and getattr(automation, method_attr) in self.method_id_meta_mapper + ) + + if not method_enabled: + self.on_assets_not_method_enabled(assets, method_type) + def get_runners(self): assets_group_by_platform = self.get_assets_group_by_platform() if settings.DEBUG_DEV: - msg = 'Assets group by platform: {}'.format(dict(assets_group_by_platform)) + msg = "Assets group by platform: {}".format(dict(assets_group_by_platform)) print(msg) runners = [] for platform, assets in assets_group_by_platform.items(): if not assets: continue - if not platform.automation or not platform.automation.ansible_enabled: - print(_(" - Platform {} ansible disabled").format(platform.name)) + + if not self.check_automation_enabled(platform, assets): continue - assets_bulked = [assets[i:i + self.bulk_size] for i in range(0, len(assets), self.bulk_size)] + # 避免一个任务太大,分批执行 + assets_bulked = [ + assets[i : i + self.bulk_size] + for i in range(0, len(assets), self.bulk_size) + ] for i, _assets in enumerate(assets_bulked, start=1): - sub_dir = '{}_{}'.format(platform.name, i) + sub_dir = "{}_{}".format(platform.name, i) playbook_dir = os.path.join(self.runtime_dir, sub_dir) - inventory_path = os.path.join(self.runtime_dir, sub_dir, 'hosts.json') + inventory_path = os.path.join(self.runtime_dir, sub_dir, "hosts.json") - method_id = getattr(platform.automation, '{}_method'.format(self.__class__.method_type())) + # method_id = getattr( + # platform.automation, + # "{}_method".format(self.__class__.method_type()), + # ) method = self.method_id_meta_mapper.get(method_id) - if not method: - logger.error("Method not found: {}".format(method_id)) - continue - - protocol = method.get('protocol') + protocol = method.get("protocol") self.generate_inventory(_assets, inventory_path, protocol) playbook_path = self.generate_playbook(method, playbook_dir) + if not playbook_path: + self.on_playbook_not_found(_assets) continue runer = SuperPlaybookRunner( @@ -362,9 +406,9 @@ class BasePlaybookManager(BaseManager): callback=PlaybookCallback(), ) - with open(inventory_path, 'r') as f: + with open(inventory_path, "r") as f: inventory_data = json.load(f) - if not inventory_data['all'].get('hosts'): + if not inventory_data["all"].get("hosts"): continue runners.append(runer) @@ -375,14 +419,14 @@ class BasePlaybookManager(BaseManager): def on_host_error(self, host, error, result): if settings.DEBUG_DEV: - print('host error: {} -> {}'.format(host, error)) + print("host error: {} -> {}".format(host, error)) def _on_host_success(self, host, result, hosts): - self.on_host_success(host, result.get("ok", '')) + self.on_host_success(host, result.get("ok", "")) def _on_host_error(self, host, result, hosts): - error = hosts.get(host, '') - detail = result.get('failures', '') or result.get('dark', '') + error = hosts.get(host, "") + detail = result.get("failures", "") or result.get("dark", "") self.on_host_error(host, error, detail) def on_runner_success(self, runner, cb): @@ -390,9 +434,9 @@ class BasePlaybookManager(BaseManager): for state, hosts in summary.items(): # 错误行为为,host 是 dict, ok 时是 list - if state == 'ok': + if state == "ok": handler = self._on_host_success - elif state == 'skipped': + elif state == "skipped": continue else: handler = self._on_host_error @@ -413,7 +457,11 @@ class BasePlaybookManager(BaseManager): print(_(">>> Task preparation phase"), end="\n") runners = self.get_runners() if len(runners) > 1: - print(_(">>> Executing tasks in batches, total {runner_count}").format(runner_count=len(runners))) + print( + _(">>> Executing tasks in batches, total {runner_count}").format( + runner_count=len(runners) + ) + ) elif len(runners) == 1: print(_(">>> Start executing tasks")) else: @@ -433,6 +481,4 @@ class BasePlaybookManager(BaseManager): self.on_runner_failed(runner, e) finally: ssh_tunnel.local_gateway_clean(runner) - print('\n') - - + print("\n") diff --git a/apps/assets/automations/methods.py b/apps/assets/automations/methods.py index c922a6d62..bad743e92 100644 --- a/apps/assets/automations/methods.py +++ b/apps/assets/automations/methods.py @@ -74,6 +74,8 @@ def sorted_methods(methods): BASE_DIR = os.path.dirname(os.path.abspath(__file__)) platform_automation_methods = get_platform_automation_methods(BASE_DIR) +print("platform_automation_methods: ") +print(json.dumps(platform_automation_methods, indent=4)) if __name__ == '__main__': print(json.dumps(platform_automation_methods, indent=4))
{{ forloop.counter }}{{ account.asset }}{{ account.username }}{% trans 'Week password' %}{{ risk.asset }}{{ risk.username }}{{ risk.risk }}