From 091bffa6263a025197e03c0ce8cde79df0867a6d Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Thu, 20 Oct 2022 20:34:15 +0800 Subject: [PATCH 1/2] perf: automation change secret linux --- apps/assets/automations/base/manager.py | 2 +- .../change_secret/host/linux/main.yml | 40 ++++++++++--- .../change_secret/host/windows/main.yml | 5 +- .../automations/change_secret/manager.py | 60 +++++++++++++++---- apps/ops/ansible/inventory.py | 23 ++++--- 5 files changed, 96 insertions(+), 34 deletions(-) diff --git a/apps/assets/automations/base/manager.py b/apps/assets/automations/base/manager.py index ad8104127..d093d22eb 100644 --- a/apps/assets/automations/base/manager.py +++ b/apps/assets/automations/base/manager.py @@ -77,9 +77,9 @@ class BasePlaybookManager: def generate_inventory(self, platformed_assets, inventory_path): inventory = JMSInventory( + manager=self, assets=platformed_assets, account_policy=self.ansible_account_policy, - host_callback=self.host_callback ) inventory.write_to_file(inventory_path) diff --git a/apps/assets/automations/change_secret/host/linux/main.yml b/apps/assets/automations/change_secret/host/linux/main.yml index 39c5d0996..cc0e1ae61 100644 --- a/apps/assets/automations/change_secret/host/linux/main.yml +++ b/apps/assets/automations/change_secret/host/linux/main.yml @@ -3,24 +3,36 @@ tasks: - name: Test privileged account ansible.builtin.ping: -# -# - name: print variables -# debug: -# msg: "Username: {{ account.username }}, Secret: {{ account.secret }}, Secret type: {{ account.secret_type }}" + # + # - name: print variables + # debug: + # msg: "Username: {{ account.username }}, Secret: {{ account.secret }}, Secret type: {{ secret_type }}" - name: Change password ansible.builtin.user: name: "{{ account.username }}" password: "{{ account.secret | password_hash('sha512') }}" update_password: always - when: account.secret_type == "password" + when: "{{ secret_type == 'password' }}" - - name: Change public key + - name: create user If it already exists, no operation will be performed + ansible.builtin.user: + name: "{{ account.username }}" + when: "{{ secret_type == 'ssh_key' }}" + + - name: remove jumpserver ssh key + ansible.builtin.lineinfile: + dest: "{{ kwargs.dest }}" + regexp: "{{ kwargs.regexp }}" + state: absent + when: "{{ secret_type == 'ssh_key' and kwargs.strategy == 'set_jms' }}" + + - name: Change SSH key ansible.builtin.authorized_key: user: "{{ account.username }}" - key: "{{ account.public_key }}" - state: present - when: account.secret_type == "public_key" + key: "{{ account.secret }}" + exclusive: "{{ kwargs.exclusive }}" + when: "{{ secret_type == 'ssh_key' }}" - name: Refresh connection ansible.builtin.meta: reset_connection @@ -32,3 +44,13 @@ ansible_user: "{{ account.username }}" ansible_password: "{{ account.secret }}" ansible_become: no + when: "{{ secret_type == 'password' }}" + + - name: Verify SSH key + ansible.builtin.ping: + become: no + vars: + ansible_user: "{{ account.username }}" + ansible_ssh_private_key_file: "{{ account.private_key_path }}" + ansible_become: no + when: "{{ secret_type == 'ssh_key' }}" diff --git a/apps/assets/automations/change_secret/host/windows/main.yml b/apps/assets/automations/change_secret/host/windows/main.yml index bc66486dc..8a2e08363 100644 --- a/apps/assets/automations/change_secret/host/windows/main.yml +++ b/apps/assets/automations/change_secret/host/windows/main.yml @@ -1,7 +1,7 @@ - hosts: demo gather_facts: no tasks: - - name: ping + - name: Test privileged account ansible.windows.win_ping: # - name: Print variables @@ -13,7 +13,7 @@ name: "{{ account.username }}" password: "{{ account.secret }}" update_password: always - when: account.secret_type == "password" + when: "{{ account.secret_type == 'password' }}" - name: Refresh connection ansible.builtin.meta: reset_connection @@ -23,3 +23,4 @@ vars: ansible_user: "{{ account.username }}" ansible_password: "{{ account.secret }}" + when: "{{ account.secret_type == 'password' }}" diff --git a/apps/assets/automations/change_secret/manager.py b/apps/assets/automations/change_secret/manager.py index fcb663ae8..80ca26d72 100644 --- a/apps/assets/automations/change_secret/manager.py +++ b/apps/assets/automations/change_secret/manager.py @@ -1,14 +1,17 @@ +import os import random import string +from hashlib import md5 from copy import deepcopy +from socket import gethostname from collections import defaultdict from django.utils import timezone -from common.utils import lazyproperty, gen_key_pair +from common.utils import lazyproperty, gen_key_pair, ssh_pubkey_gen, ssh_key_string_to_obj from assets.models import ChangeSecretRecord from assets.const import ( - AutomationTypes, SecretType, SecretStrategy, DEFAULT_PASSWORD_RULES + AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy, DEFAULT_PASSWORD_RULES ) from ..base.manager import BasePlaybookManager @@ -17,15 +20,15 @@ class ChangeSecretManager(BasePlaybookManager): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.method_hosts_mapper = defaultdict(list) + self.secret_type = self.execution.plan_snapshot.get('secret_type') self.secret_strategy = self.execution.plan_snapshot['secret_strategy'] - self.ssh_key_change_strategy = self.execution.plan_snapshot['ssh_key_change_strategy'] self._password_generated = None self._ssh_key_generated = None self.name_recorder_mapper = {} # 做个映射,方便后面处理 @classmethod def method_type(cls): - return AutomationTypes.change_secret + return AutomationTypes.method_id_meta_mapper @lazyproperty def related_accounts(self): @@ -36,6 +39,19 @@ class ChangeSecretManager(BasePlaybookManager): private_key, public_key = gen_key_pair() return private_key + @staticmethod + def generate_public_key(private_key): + return ssh_pubkey_gen(private_key=private_key, hostname=gethostname()) + + @staticmethod + def generate_private_key_path(secret, path_dir): + key_name = '.' + md5(secret.encode('utf-8')).hexdigest() + key_path = os.path.join(path_dir, key_name) + if not os.path.exists(key_path): + ssh_key_string_to_obj(secret, password=None).write_private_key_file(key_path) + os.chmod(key_path, 0o400) + return key_path + def generate_password(self): kwargs = self.automation.plan_snapshot['password_rules'] or {} length = int(kwargs.get('length', DEFAULT_PASSWORD_RULES['length'])) @@ -77,16 +93,29 @@ class ChangeSecretManager(BasePlaybookManager): else: return self.generate_password() - def get_secret(self, account): - if account.secret_type == SecretType.ssh_key: + def get_secret(self): + if self.secret_type == SecretType.ssh_key: secret = self.get_ssh_key() - elif account.secret_type == SecretType.password: + elif self.secret_type == SecretType.password: secret = self.get_password() else: raise ValueError("Secret must be set") return secret - def host_callback(self, host, asset=None, account=None, automation=None, **kwargs): + def get_kwargs(self, account, secret): + kwargs = {} + if self.secret_type != SecretType.ssh_key: + return kwargs + kwargs['strategy'] = self.automation.plan_snapshot['ssh_key_change_strategy'] + kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no' + + if kwargs['strategy'] == SSHKeyStrategy.set_jms: + kwargs['dest'] = '/home/{}/.ssh/authorized_keys'.format(account.username) + kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip()) + + return kwargs + + def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs): host = super().host_callback(host, asset=asset, account=account, automation=automation, **kwargs) if host.get('error'): return host @@ -95,7 +124,9 @@ class ChangeSecretManager(BasePlaybookManager): if account: accounts = accounts.exclude(id=account.id) if '*' not in self.automation.accounts: - accounts = accounts.filter(username__in=self.automation.accounts) + accounts = accounts.filter( + username__in=self.automation.accounts, secret_type=self.secret_type + ) method_attr = getattr(automation, self.method_type() + '_method') method_hosts = self.method_hosts_mapper[method_attr] @@ -103,11 +134,12 @@ class ChangeSecretManager(BasePlaybookManager): inventory_hosts = [] records = [] + host['secret_type'] = self.secret_type for account in accounts: h = deepcopy(host) h['name'] += '_' + account.username + new_secret = self.get_secret() - new_secret = self.get_secret(account) recorder = ChangeSecretRecord( account=account, execution=self.execution, old_secret=account.secret, new_secret=new_secret, @@ -115,11 +147,19 @@ class ChangeSecretManager(BasePlaybookManager): records.append(recorder) self.name_recorder_mapper[h['name']] = recorder + private_key_path = None + if self.secret_type == SecretType.ssh_key: + private_key_path = self.generate_private_key_path(new_secret, path_dir) + new_secret = self.generate_public_key(new_secret) + + h['kwargs'] = self.get_kwargs(account, new_secret) + h['account'] = { 'name': account.name, 'username': account.username, 'secret_type': account.secret_type, 'secret': new_secret, + 'private_key_path': private_key_path } inventory_hosts.append(h) method_hosts.append(h['name']) diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py index f6c19b7a2..8f555eb85 100644 --- a/apps/ops/ansible/inventory.py +++ b/apps/ops/ansible/inventory.py @@ -5,28 +5,26 @@ from collections import defaultdict from django.utils.translation import gettext as _ - __all__ = ['JMSInventory'] class JMSInventory: - def __init__(self, assets, account_policy='smart', account_prefer='root,administrator', host_callback=None): + def __init__(self, manager, assets=None, account_policy='smart', account_prefer='root,administrator'): """ :param assets: :param account_prefer: account username name if not set use account_policy - :param account_policy: - :param host_callback: after generate host, call this callback to modify host + :param account_policy: smart, privileged_must, privileged_first """ + self.manager = manager self.assets = self.clean_assets(assets) self.account_prefer = account_prefer self.account_policy = account_policy - self.host_callback = host_callback @staticmethod def clean_assets(assets): from assets.models import Asset asset_ids = [asset.id for asset in assets] - assets = Asset.objects.filter(id__in=asset_ids, is_active=True)\ + assets = Asset.objects.filter(id__in=asset_ids, is_active=True) \ .prefetch_related('platform', 'domain', 'accounts') return assets @@ -107,7 +105,7 @@ class JMSInventory: 'protocol': asset.protocol, 'port': asset.port, 'protocols': [{'name': p.name, 'port': p.port} for p in protocols], }, - 'jms_account': { + 'jms_account': { 'id': str(account.id), 'username': account.username, 'secret': account.secret, 'secret_type': account.secret_type } if account else None @@ -156,7 +154,7 @@ class JMSInventory: account_selected = accounts[0] if accounts else None return account_selected - def generate(self): + def generate(self, path_dir): hosts = [] platform_assets = self.group_by_platform(self.assets) for platform, assets in platform_assets.items(): @@ -170,10 +168,11 @@ class JMSInventory: if not automation.ansible_enabled: host['error'] = _('Ansible disabled') - if self.host_callback is not None: - host = self.host_callback( + if self.manager.host_callback is not None: + host = self.manager.host_callback( host, asset=asset, account=account, - platform=platform, automation=automation + platform=platform, automation=automation, + path_dir=path_dir ) if isinstance(host, list): @@ -195,8 +194,8 @@ class JMSInventory: return data def write_to_file(self, path): - data = self.generate() path_dir = os.path.dirname(path) + data = self.generate(path_dir) if not os.path.exists(path_dir): os.makedirs(path_dir, 0o700, True) with open(path, 'w') as f: From 64daacce634d6504971dcbcb20e146deece43ab5 Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Fri, 21 Oct 2022 18:19:09 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=8C=96=E4=BF=AE=E6=94=B9=E5=AF=86=E7=A0=81bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/automations/base/manager.py | 2 +- .../change_secret/host/linux/main.yml | 12 +++++----- .../change_secret/host/windows/main.yml | 4 ++-- .../automations/change_secret/manager.py | 23 +++++++++---------- apps/assets/models/automations/base.py | 11 +++++---- .../models/automations/change_secret.py | 2 +- apps/ops/ansible/inventory.py | 6 ++++- 7 files changed, 33 insertions(+), 27 deletions(-) diff --git a/apps/assets/automations/base/manager.py b/apps/assets/automations/base/manager.py index d093d22eb..e2faecd64 100644 --- a/apps/assets/automations/base/manager.py +++ b/apps/assets/automations/base/manager.py @@ -49,7 +49,7 @@ class BasePlaybookManager: ansible_dir = settings.ANSIBLE_DIR dir_name = '{}_{}'.format(self.automation.name.replace(' ', '_'), self.execution.id) path = os.path.join( - ansible_dir, 'automations', self.automation.type, + ansible_dir, 'automations', self.execution.snapshot['type'], dir_name, timezone.now().strftime('%Y%m%d_%H%M%S') ) if not os.path.exists(path): diff --git a/apps/assets/automations/change_secret/host/linux/main.yml b/apps/assets/automations/change_secret/host/linux/main.yml index cc0e1ae61..d295079ba 100644 --- a/apps/assets/automations/change_secret/host/linux/main.yml +++ b/apps/assets/automations/change_secret/host/linux/main.yml @@ -13,26 +13,26 @@ name: "{{ account.username }}" password: "{{ account.secret | password_hash('sha512') }}" update_password: always - when: "{{ secret_type == 'password' }}" + when: secret_type == "password" - name: create user If it already exists, no operation will be performed ansible.builtin.user: name: "{{ account.username }}" - when: "{{ secret_type == 'ssh_key' }}" + when: secret_type == "ssh_key" - name: remove jumpserver ssh key ansible.builtin.lineinfile: dest: "{{ kwargs.dest }}" regexp: "{{ kwargs.regexp }}" state: absent - when: "{{ secret_type == 'ssh_key' and kwargs.strategy == 'set_jms' }}" + when: secret_type == "ssh_key" and kwargs.strategy == "set_jms" - name: Change SSH key ansible.builtin.authorized_key: user: "{{ account.username }}" key: "{{ account.secret }}" exclusive: "{{ kwargs.exclusive }}" - when: "{{ secret_type == 'ssh_key' }}" + when: secret_type == "ssh_key" - name: Refresh connection ansible.builtin.meta: reset_connection @@ -44,7 +44,7 @@ ansible_user: "{{ account.username }}" ansible_password: "{{ account.secret }}" ansible_become: no - when: "{{ secret_type == 'password' }}" + when: secret_type == "password" - name: Verify SSH key ansible.builtin.ping: @@ -53,4 +53,4 @@ ansible_user: "{{ account.username }}" ansible_ssh_private_key_file: "{{ account.private_key_path }}" ansible_become: no - when: "{{ secret_type == 'ssh_key' }}" + when: secret_type == "ssh_key" diff --git a/apps/assets/automations/change_secret/host/windows/main.yml b/apps/assets/automations/change_secret/host/windows/main.yml index 8a2e08363..0c27301dc 100644 --- a/apps/assets/automations/change_secret/host/windows/main.yml +++ b/apps/assets/automations/change_secret/host/windows/main.yml @@ -13,7 +13,7 @@ name: "{{ account.username }}" password: "{{ account.secret }}" update_password: always - when: "{{ account.secret_type == 'password' }}" + when: account.secret_type == "password" - name: Refresh connection ansible.builtin.meta: reset_connection @@ -23,4 +23,4 @@ vars: ansible_user: "{{ account.username }}" ansible_password: "{{ account.secret }}" - when: "{{ account.secret_type == 'password' }}" + when: account.secret_type == "password" diff --git a/apps/assets/automations/change_secret/manager.py b/apps/assets/automations/change_secret/manager.py index 80ca26d72..954a309b5 100644 --- a/apps/assets/automations/change_secret/manager.py +++ b/apps/assets/automations/change_secret/manager.py @@ -20,15 +20,15 @@ class ChangeSecretManager(BasePlaybookManager): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.method_hosts_mapper = defaultdict(list) - self.secret_type = self.execution.plan_snapshot.get('secret_type') - self.secret_strategy = self.execution.plan_snapshot['secret_strategy'] + self.secret_type = self.execution.snapshot['secret_type'] + self.secret_strategy = self.execution.snapshot['secret_strategy'] self._password_generated = None self._ssh_key_generated = None self.name_recorder_mapper = {} # 做个映射,方便后面处理 @classmethod def method_type(cls): - return AutomationTypes.method_id_meta_mapper + return AutomationTypes.change_secret @lazyproperty def related_accounts(self): @@ -53,7 +53,7 @@ class ChangeSecretManager(BasePlaybookManager): return key_path def generate_password(self): - kwargs = self.automation.plan_snapshot['password_rules'] or {} + kwargs = self.execution.snapshot['password_rules'] or {} length = int(kwargs.get('length', DEFAULT_PASSWORD_RULES['length'])) symbol_set = kwargs.get('symbol_set') if symbol_set is None: @@ -69,7 +69,7 @@ class ChangeSecretManager(BasePlaybookManager): def get_ssh_key(self): if self.secret_strategy == SecretStrategy.custom: - ssh_key = self.automation.plan_snapshot['ssh_key'] + ssh_key = self.execution.snapshot['ssh_key'] if not ssh_key: raise ValueError("Automation SSH key must be set") return ssh_key @@ -82,7 +82,7 @@ class ChangeSecretManager(BasePlaybookManager): def get_password(self): if self.secret_strategy == SecretStrategy.custom: - password = self.automation.plan_snapshot['password'] + password = self.execution.snapshot['secret'] if not password: raise ValueError("Automation Password must be set") return password @@ -106,7 +106,7 @@ class ChangeSecretManager(BasePlaybookManager): kwargs = {} if self.secret_type != SecretType.ssh_key: return kwargs - kwargs['strategy'] = self.automation.plan_snapshot['ssh_key_change_strategy'] + kwargs['strategy'] = self.execution.snapshot['ssh_key_change_strategy'] kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no' if kwargs['strategy'] == SSHKeyStrategy.set_jms: @@ -123,11 +123,11 @@ class ChangeSecretManager(BasePlaybookManager): accounts = asset.accounts.all() if account: accounts = accounts.exclude(id=account.id) - if '*' not in self.automation.accounts: - accounts = accounts.filter( - username__in=self.automation.accounts, secret_type=self.secret_type - ) + if '*' not in self.execution.snapshot['accounts']: + accounts = accounts.filter(username__in=self.execution.snapshot['accounts']) + + accounts = accounts.filter(secret_type=self.secret_type) method_attr = getattr(automation, self.method_type() + '_method') method_hosts = self.method_hosts_mapper[method_attr] method_hosts = [h for h in method_hosts if h != host['name']] @@ -153,7 +153,6 @@ class ChangeSecretManager(BasePlaybookManager): new_secret = self.generate_public_key(new_secret) h['kwargs'] = self.get_kwargs(account, new_secret) - h['account'] = { 'name': account.name, 'username': account.username, diff --git a/apps/assets/models/automations/base.py b/apps/assets/models/automations/base.py index b0fbd80a2..904b98717 100644 --- a/apps/assets/models/automations/base.py +++ b/apps/assets/models/automations/base.py @@ -40,15 +40,18 @@ class BaseAutomation(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin): def get_register_task(self): raise NotImplementedError + def get_many_to_many_ids(self, field: str): + return [str(i) for i in getattr(self, field).all().values_list('id', flat=True)] + def to_attr_json(self): return { 'name': self.name, 'type': self.type, - 'org_id': self.org_id, + 'org_id': str(self.org_id), 'comment': self.comment, 'accounts': self.accounts, - 'nodes': list(self.nodes.all().values_list('id', flat=True)), - 'assets': list(self.assets.all().values_list('id', flat=True)), + 'nodes': self.get_many_to_many_ids('nodes'), + 'assets': self.get_many_to_many_ids('assets'), } def execute(self, trigger=Trigger.manual): @@ -59,7 +62,7 @@ class BaseAutomation(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin): execution = self.executions.model.objects.create( id=eid, trigger=trigger, automation=self, - plan_snapshot=self.to_attr_json(), + snapshot=self.to_attr_json(), ) return execution.start() diff --git a/apps/assets/models/automations/change_secret.py b/apps/assets/models/automations/change_secret.py index 47462320d..5624caf1f 100644 --- a/apps/assets/models/automations/change_secret.py +++ b/apps/assets/models/automations/change_secret.py @@ -18,7 +18,7 @@ class ChangeSecretAutomation(BaseAutomation): ) secret_strategy = models.CharField( choices=SecretStrategy.choices, max_length=16, - default=SecretStrategy.random_one, verbose_name=_('Secret strategy') + default=SecretStrategy.custom, verbose_name=_('Secret strategy') ) secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret')) password_rules = models.JSONField(default=dict, verbose_name=_('Password rules')) diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py index 8f555eb85..09427f3e5 100644 --- a/apps/ops/ansible/inventory.py +++ b/apps/ops/ansible/inventory.py @@ -61,6 +61,7 @@ class JMSInventory: var = { 'ansible_user': account.username, } + if not account.secret: return var if account.secret_type == 'password': @@ -77,7 +78,10 @@ class JMSInventory: ssh_protocol_matched = list(filter(lambda x: x.name == 'ssh', protocols)) ssh_protocol = ssh_protocol_matched[0] if ssh_protocol_matched else None host['ansible_host'] = asset.address - host['ansible_port'] = ssh_protocol.port if ssh_protocol else 22 + if asset.port == 0: + host['ansible_port'] = ssh_protocol.port if ssh_protocol else 22 + else: + host['ansible_port'] = asset.port su_from = account.su_from if platform.su_enabled and su_from: