perf: gather accounts

pull/8997/head
feng 2022-10-27 18:53:10 +08:00
parent 097ebc2362
commit 2355d1af83
28 changed files with 328 additions and 103 deletions

View File

@ -27,6 +27,7 @@
password: "{{ account.secret }}"
host: "%"
when: db_info is succeeded
register: change_info
- name: Verify password
community.mysql.mysql_info:
@ -35,3 +36,6 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
filter: version
when:
- db_info is succeeded
- change_info is succeeded

View File

@ -1,8 +1,8 @@
- hosts: postgre
gather_facts: no
vars:
# ansible_python_interpreter: /usr/local/bin/python
ansible_python_interpreter: /Users/xiaofeng/Desktop/jumpserver/venv/bin/python
ansible_python_interpreter: /usr/local/bin/python
tasks:
- name: Test PostgreSQL connection
community.postgresql.postgresql_ping:
@ -37,4 +37,6 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
db: "{{ jms_asset.database }}"
when: db_info is succeeded and change_info is changed
when:
- db_info is succeeded
- change_info is succeeded

View File

@ -1,29 +1,58 @@
- hosts: demo
gather_facts: no
tasks:
- name: ping
- name: Test privileged account
ansible.builtin.ping:
#- name: print variables
# debug:
# msg: "Username: {{ account.username }}, Password: {{ account.password }}"
#
# - name: print variables
# debug:
# msg: "Username: {{ account.username }}, Secret: {{ account.secret }}, Secret type: {{ secret_type }}"
- name: Change password
user:
ansible.builtin.user:
name: "{{ account.username }}"
password: "{{ account.password | password_hash('des') }}"
password: "{{ account.secret | password_hash('sha512') }}"
update_password: always
when: account.password
when: secret_type == "password"
- name: Change public key
authorized_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"
- kwargs.strategy == "set_jms"
- name: Change SSH key
ansible.builtin.authorized_key:
user: "{{ account.username }}"
key: "{{ account.public_key }}"
state: present
when: account.public_key
key: "{{ account.secret }}"
exclusive: "{{ kwargs.exclusive }}"
when: secret_type == "ssh_key"
- name: Refresh connection
ansible.builtin.meta: reset_connection
- name: Verify password
ansible.builtin.ping:
become: no
vars:
ansible_user: "{{ account.username }}"
ansible_pass: "{{ account.password }}"
ansible_ssh_connection: paramiko
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"

View File

@ -25,7 +25,9 @@
dest: "{{ kwargs.dest }}"
regexp: "{{ kwargs.regexp }}"
state: absent
when: secret_type == "ssh_key" and kwargs.strategy == "set_jms"
when:
- secret_type == "ssh_key"
- kwargs.strategy == "set_jms"
- name: Change SSH key
ansible.builtin.authorized_key:

View File

@ -1,5 +1,5 @@
id: change_secret_linux
name: Change password for Linux
id: change_secret_posix
name: Change secret for posix
category: host
type:
- unix

View File

@ -1,8 +1,6 @@
# from .backup.manager import AccountBackupExecutionManager
#
#
from .change_secret.manager import ChangeSecretManager
from .gather_facts.manager import GatherFactsManager
from .gather_accounts.manager import GatherAccountsManager
from ..const import AutomationTypes
@ -10,6 +8,7 @@ class ExecutionManager:
manager_type_mapper = {
AutomationTypes.change_secret: ChangeSecretManager,
AutomationTypes.gather_facts: GatherFactsManager,
AutomationTypes.gather_accounts: GatherAccountsManager,
}
def __init__(self, execution):

View File

@ -0,0 +1,22 @@
- hosts: mysql
gather_facts: no
vars:
# ansible_python_interpreter: /usr/local/bin/python
ansible_python_interpreter: /Users/xiaofeng/Desktop/jumpserver/venv/bin/python
tasks:
- name: Get info
community.mysql.mysql_info:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
filter: users
register: db_info
- name: Define info by set_fact
set_fact:
info: "{{ db_info.users }}"
- debug:
var: info

View File

@ -0,0 +1,6 @@
id: gather_accounts_mysql
name: Gather account from MySQL
category: database
type:
- mysql
method: gather_accounts

View File

@ -0,0 +1,23 @@
- hosts: postgresql
gather_facts: no
vars:
# ansible_python_interpreter: /usr/local/bin/python
ansible_python_interpreter: /Users/xiaofeng/Desktop/jumpserver/venv/bin/python
tasks:
- name: Get info
community.postgresql.postgresql_info:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
login_db: "{{ jms_asset.database }}"
filter: "roles"
register: db_info
- name: Define info by set_fact
set_fact:
info: "{{ db_info.roles }}"
- debug:
var: info

View File

@ -0,0 +1,6 @@
id: gather_accounts_postgresql
name: Gather account for PostgreSQL
category: database
type:
- postgresql
method: gather_accounts

View File

@ -0,0 +1,56 @@
from django.utils import timezone
__all__ = ['GatherAccountsFilter']
# TODO 后期会挪到playbook中
class GatherAccountsFilter:
def __init__(self, tp):
self.tp = tp
@staticmethod
def mysql_filter(info):
result = {}
for _, user_dict in info.items():
for username, data in user_dict.items():
if data.get('account_locked') == 'N':
result[username] = {}
return result
@staticmethod
def postgresql_filter(info):
result = {}
for username in info:
result[username] = {}
return result
@staticmethod
def posix_filter(info):
result = {}
for line in info:
data = line.split('@')
if len(data) != 3:
continue
username, address, dt = data
date = timezone.datetime.strptime(f'{dt} +0800', '%b %d %H:%M:%S %Y %z')
result[username] = {'address': address, 'date': date}
return result
@staticmethod
def windows_filter(info):
# TODO
result = {}
return result
def run(self, method_id_meta_mapper, info):
run_method_name = None
for k, v in method_id_meta_mapper.items():
if self.tp not in v['type']:
continue
run_method_name = k.replace(f'{v["method"]}_', '')
if not run_method_name:
return info
return getattr(self, f'{run_method_name}_filter')(info)

View File

@ -0,0 +1,14 @@
- hosts: demo
gather_facts: no
tasks:
- name: Gather posix account
ansible.builtin.win_shell:
cmd: net user
register: result
- name: Define info by set_fact
ansible.builtin.set_fact:
info: "{{ result.stdout_lines }}"
- debug:
var: info

View File

@ -0,0 +1,7 @@
id: gather_accounts_posix
name: Gather posix account
category: host
type:
- linux
- unix
method: gather_accounts

View File

@ -0,0 +1,18 @@
- hosts: windows
gather_facts: yes
tasks:
- name: Get info
set_fact:
info:
arch: "{{ ansible_architecture2 }}"
distribution: "{{ ansible_distribution }}"
distribution_version: "{{ ansible_distribution_version }}"
kernel: "{{ ansible_kernel }}"
vendor: "{{ ansible_system_vendor }}"
model: "{{ ansible_product_name }}"
sn: "{{ ansible_product_serial }}"
cpu_vcpus: "{{ ansible_processor_vcpus }}"
memory: "{{ ansible_memtotal_mb }}"
- debug:
var: info

View File

@ -0,0 +1,7 @@
id: gather_accounts_windows
name: Gather account windows
version: 1
method: gather_accounts
category: host
type:
- windows

View File

@ -0,0 +1,45 @@
from common.utils import get_logger
from assets.const import AutomationTypes
from orgs.utils import tmp_to_org
from .filter import GatherAccountsFilter
from ...models import Account, GatheredUser
from ..base.manager import BasePlaybookManager
logger = get_logger(__name__)
class GatherAccountsManager(BasePlaybookManager):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.host_asset_mapper = {}
@classmethod
def method_type(cls):
return AutomationTypes.gather_accounts
def host_callback(self, host, asset=None, **kwargs):
super().host_callback(host, asset=asset, **kwargs)
self.host_asset_mapper[host['name']] = asset
return host
def filter_success_result(self, host, result):
result = GatherAccountsFilter(host).run(self.method_id_meta_mapper, result)
return result
def on_host_success(self, host, result):
info = result.get('debug', {}).get('res', {}).get('info', {})
asset = self.host_asset_mapper.get(host)
org_id = asset.org_id
if asset and info:
result = self.filter_success_result(host, info)
with tmp_to_org(org_id):
GatheredUser.objects.filter(asset=asset, present=True).update(present=False)
for username, data in result.items():
defaults = {'asset': asset, 'present': True, 'username': username}
if data.get('date'):
defaults['date_last_login'] = data['date']
if data.get('address'):
defaults['ip_last_login'] = data['address'][:32]
GatheredUser.objects.update_or_create(defaults=defaults, asset=asset, username=username)
else:
logger.error("Not found info, task name must be 'Get info': {}".format(host))

View File

@ -13,7 +13,7 @@
filter: version
register: db_info
- name: Define Mysql info by set_fact
- name: Define info by set_fact
set_fact:
info:
version: "{{ db_info.version.full }}"

View File

@ -1,4 +1,4 @@
- hosts: postgre
- hosts: postgresql
gather_facts: no
vars:
ansible_python_interpreter: /usr/local/bin/python
@ -13,7 +13,7 @@
login_db: "{{ jms_asset.database }}"
register: db_info
- name: Define Postgresql info by set_fact
- name: Define info by set_fact
set_fact:
info:
version: "{{ db_info.server_version.raw }}"

View File

@ -1,4 +1,4 @@
- hosts: website
- hosts: demo
gather_facts: yes
tasks:
- name: Get info

View File

@ -1,75 +1,9 @@
import os
import shutil
from copy import deepcopy
from collections import defaultdict
import yaml
from django.utils.translation import gettext as _
from ops.ansible import PlaybookRunner
from common.utils import get_logger
from assets.const import AutomationTypes
from ..base.manager import BasePlaybookManager
from assets.automations.methods import platform_automation_methods
class ChangePasswordManager(BasePlaybookManager):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.id_method_mapper = {
method['id']: method
for method in platform_automation_methods
}
self.method_hosts_mapper = defaultdict(list)
self.playbooks = []
def inventory_kwargs(self):
return {
'host_callback': self.host_duplicator
}
def generate_playbook(self):
playbook = []
for method_id, host_names in self.method_hosts_mapper.items():
method = self.id_method_mapper[method_id]
method_playbook_dir_path = method['dir']
method_playbook_dir_name = os.path.basename(method_playbook_dir_path)
sub_playbook_dir = os.path.join(os.path.dirname(self.playbook_path), method_playbook_dir_name)
shutil.copytree(method_playbook_dir_path, sub_playbook_dir)
sub_playbook_path = os.path.join(sub_playbook_dir, 'main.yml')
with open(sub_playbook_path, 'r') as f:
host_playbook_play = yaml.safe_load(f)
if isinstance(host_playbook_play, list):
host_playbook_play = host_playbook_play[0]
step = 10
hosts_grouped = [host_names[i:i+step] for i in range(0, len(host_names), step)]
for i, hosts in enumerate(hosts_grouped):
plays = []
play = deepcopy(host_playbook_play)
play['hosts'] = ':'.join(hosts)
plays.append(play)
playbook_path = os.path.join(sub_playbook_dir, 'part_{}.yml'.format(i))
with open(playbook_path, 'w') as f:
yaml.safe_dump(plays, f)
self.playbooks.append(playbook_path)
playbook.append({
'name': method['name'] + ' for part {}'.format(i),
'import_playbook': os.path.join(method_playbook_dir_name, 'part_{}.yml'.format(i))
})
with open(self.playbook_path, 'w') as f:
yaml.safe_dump(playbook, f)
print("Generate playbook done: " + self.playbook_path)
def get_runner(self):
return PlaybookRunner(
self.inventory_path,
self.playbook_path,
self.runtime_dir
)
logger = get_logger(__name__)
class PingManager(BasePlaybookManager):
pass

View File

@ -15,7 +15,7 @@ class AutomationTypes(TextChoices):
push_account = 'push_account', _('Create account')
change_secret = 'change_secret', _('Change secret')
verify_account = 'verify_account', _('Verify account')
gather_account = 'gather_account', _('Gather account')
gather_accounts = 'gather_accounts', _('Gather accounts')
class SecretStrategy(TextChoices):

View File

@ -166,6 +166,7 @@ class AllTypes(ChoicesMixin):
data['protocols'] = protocols
automation = constraints.get('automation', {})
enable_fields = {k: v for k, v in automation.items() if k.endswith('_enabled')}
for k, v in enable_fields.items():
auto_item = k.replace('_enabled', '')

View File

@ -0,0 +1,29 @@
# Generated by Django 3.2.14 on 2022-10-27 02:53
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0110_auto_20221021_1506'),
]
operations = [
migrations.CreateModel(
name='GatherAccountsAutomation',
fields=[
('baseautomation_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='assets.baseautomation')),
],
options={
'verbose_name': 'Gather asset accounts',
},
bases=('assets.baseautomation',),
),
migrations.AlterField(
model_name='baseautomation',
name='type',
field=models.CharField(choices=[('ping', 'Ping'), ('gather_facts', 'Gather facts'), ('push_account', 'Create account'), ('change_secret', 'Change secret'), ('verify_account', 'Verify account'), ('gather_accounts', 'Gather accounts')], max_length=16, verbose_name='Type'),
),
]

View File

@ -3,3 +3,4 @@ from .discovery_account import *
from .push_account import *
from .verify_secret import *
from .gather_facts import *
from .gather_accounts import *

View File

@ -10,6 +10,7 @@ from orgs.mixins.models import OrgModelMixin
from ops.mixin import PeriodTaskModelMixin
from assets.models import Node, Asset
from assets.tasks import execute_automation
from assets.const import AutomationTypes
class BaseAutomation(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin):
@ -20,7 +21,7 @@ class BaseAutomation(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin):
assets = models.ManyToManyField(
'assets.Asset', blank=True, verbose_name=_("Assets")
)
type = models.CharField(max_length=16, verbose_name=_('Type'))
type = models.CharField(max_length=16, choices=AutomationTypes.choices, verbose_name=_('Type'))
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
comment = models.TextField(blank=True, verbose_name=_('Comment'))

View File

@ -0,0 +1,15 @@
from django.utils.translation import ugettext_lazy as _
from assets.const import AutomationTypes
from .base import BaseAutomation
__all__ = ['GatherAccountsAutomation']
class GatherAccountsAutomation(BaseAutomation):
def save(self, *args, **kwargs):
self.type = AutomationTypes.gather_accounts
super().save(*args, **kwargs)
class Meta:
verbose_name = _("Gather asset accounts")

View File

@ -106,7 +106,7 @@ class JMSInventory:
'jms_asset': {
'id': str(asset.id), 'name': asset.name, 'address': asset.address,
'type': asset.type, 'category': asset.category,
'protocol': asset.protocol, 'port': asset.port,
'protocol': asset.protocol, 'port': asset.port,'database': '',
'protocols': [{'name': p.name, 'port': p.port} for p in protocols],
},
'jms_account': {
@ -117,6 +117,10 @@ class JMSInventory:
ansible_config = dict(automation.ansible_config)
ansible_connection = ansible_config.get('ansible_connection', 'ssh')
host.update(ansible_config)
if platform.category == 'database':
host['jms_asset']['database'] = asset.database.db_name
gateway = None
if asset.domain:
gateway = asset.domain.select_gateway()