Merge branch 'v3' of github.com:jumpserver/jumpserver into v3

pull/8970/head
Jiangjie.Bai 2022-10-13 18:19:56 +08:00
commit d52baf0af5
166 changed files with 7008 additions and 3186 deletions

View File

@ -18,7 +18,6 @@ __all__ = ['AccountViewSet', 'AccountSecretsViewSet', 'AccountTaskCreateAPI']
class AccountViewSet(OrgBulkModelViewSet):
model = Account
filterset_fields = ("username", "asset", 'name')
search_fields = ('username', 'asset__address', 'name')
filterset_class = AccountFilterSet
serializer_classes = {
@ -33,7 +32,7 @@ class AccountViewSet(OrgBulkModelViewSet):
@action(methods=['post'], detail=True, url_path='verify')
def verify_account(self, request, *args, **kwargs):
account = super().get_object()
task = test_accounts_connectivity_manual.delay([account])
task = test_accounts_connectivity_manual.delay([account.id])
return Response(data={'task': task.id})
@ -54,7 +53,6 @@ class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
class AccountTaskCreateAPI(CreateAPIView):
serializer_class = serializers.AccountTaskSerializer
filterset_fields = AccountViewSet.filterset_fields
search_fields = AccountViewSet.search_fields
filterset_class = AccountViewSet.filterset_class
@ -67,8 +65,8 @@ class AccountTaskCreateAPI(CreateAPIView):
return queryset
def perform_create(self, serializer):
accounts = self.get_accounts()
task = test_accounts_connectivity_manual.delay(accounts)
account_ids = self.get_accounts().values_list('id', flat=True)
task = test_accounts_connectivity_manual.delay(account_ids)
data = getattr(serializer, '_data', {})
data["task"] = task.id
setattr(serializer, '_data', data)

View File

@ -1,12 +1,8 @@
from rest_framework.decorators import action
from rest_framework.response import Response
from common.drf.api import JMSModelViewSet
from common.drf.serializers import GroupedChoiceSerializer
from assets.models import Platform
from assets.serializers import PlatformSerializer, PlatformOpsMethodSerializer
from assets.const import AllTypes
from assets.playbooks import filter_platform_methods
from assets.serializers import PlatformSerializer
__all__ = ['AssetPlatformViewSet']

View File

@ -0,0 +1 @@
from .methods import platform_automation_methods, filter_platform_methods

View File

@ -0,0 +1,14 @@
## all connection vars
hostname asset_name=name asset_type=type asset_primary_protocol=ssh asset_primary_port=22 asset_protocols=[]
## local connection
hostname ansible_connection=local
## local connection with gateway
hostname ansible_connection=ssh ansible_user=gateway.username ansible_port=gateway.port ansible_host=gateway.host ansible_ssh_private_key_file=gateway.key
## ssh connection for windows
hostname ansible_connection=ssh ansible_shell_type=powershell/cmd ansible_user=windows.username ansible_port=windows.port ansible_host=windows.host ansible_ssh_private_key_file=windows.key
## ssh connection
hostname ansible_user=user ansible_password=pass ansible_host=host ansible_port=port ansible_ssh_private_key_file=key ssh_args="-o StrictHostKeyChecking=no"

View File

@ -0,0 +1,179 @@
import os
import shutil
import yaml
from copy import deepcopy
from collections import defaultdict
from django.conf import settings
from django.utils import timezone
from django.utils.translation import gettext as _
from common.utils import get_logger
from assets.automations.methods import platform_automation_methods
from ops.ansible import JMSInventory, PlaybookRunner, DefaultCallback
logger = get_logger(__name__)
class PlaybookCallback(DefaultCallback):
def playbook_on_stats(self, event_data, **kwargs):
super().playbook_on_stats(event_data, **kwargs)
class BasePlaybookManager:
bulk_size = 100
ansible_account_policy = 'privileged_first'
def __init__(self, execution):
self.execution = execution
self.automation = execution.automation
self.method_id_meta_mapper = {
method['id']: method
for method in platform_automation_methods
if method['method'] == self.__class__.method_type()
}
# 根据执行方式就行分组, 不同资产的改密、推送等操作可能会使用不同的执行方式
# 然后根据执行方式分组, 再根据 bulk_size 分组, 生成不同的 playbook
# 避免一个 playbook 中包含太多的主机
self.method_hosts_mapper = defaultdict(list)
self.playbooks = []
@classmethod
def method_type(cls):
raise NotImplementedError
@property
def runtime_dir(self):
ansible_dir = settings.ANSIBLE_DIR
path = os.path.join(
ansible_dir, self.automation.type,
self.automation.name.replace(' ', '_'),
timezone.now().strftime('%Y%m%d_%H%M%S')
)
return path
@property
def inventory_path(self):
return os.path.join(self.runtime_dir, 'inventory', 'hosts.json')
@property
def playbook_path(self):
return os.path.join(self.runtime_dir, 'project', 'main.yml')
def generate(self):
self.prepare_playbook_dir()
self.generate_inventory()
self.generate_playbook()
def prepare_playbook_dir(self):
inventory_dir = os.path.dirname(self.inventory_path)
playbook_dir = os.path.dirname(self.playbook_path)
for d in [inventory_dir, playbook_dir]:
if not os.path.exists(d):
os.makedirs(d, exist_ok=True, mode=0o755)
def host_callback(self, host, automation=None, **kwargs):
enabled_attr = '{}_enabled'.format(self.__class__.method_type())
method_attr = '{}_method'.format(self.__class__.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'] = _('Change password disabled')
return host
self.method_hosts_mapper[getattr(automation, method_attr)].append(host['name'])
return host
def generate_inventory(self):
inventory = JMSInventory(
assets=self.automation.get_all_assets(),
account_policy=self.ansible_account_policy,
host_callback=self.host_callback
)
inventory.write_to_file(self.inventory_path)
logger.debug("Generate inventory done: {}".format(self.inventory_path))
def generate_playbook(self):
main_playbook = []
for method_id, host_names in self.method_hosts_mapper.items():
method = self.method_id_meta_mapper.get(method_id)
if not method:
logger.error("Method not found: {}".format(method_id))
continue
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)
sub_playbook_path = os.path.join(sub_playbook_dir, 'main.yml')
shutil.copytree(method_playbook_dir_path, sub_playbook_dir)
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]
hosts_bulked = [host_names[i:i+self.bulk_size] for i in range(0, len(host_names), self.bulk_size)]
for i, hosts in enumerate(hosts_bulked):
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, hosts])
main_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(main_playbook, f)
logger.debug("Generate playbook done: " + self.playbook_path)
def get_runners(self):
runners = []
for playbook_path in self.playbooks:
runer = PlaybookRunner(
self.inventory_path,
playbook_path,
self.runtime_dir,
callback=PlaybookCallback(),
)
runners.append(runer)
return runners
def on_runner_done(self, runner, cb):
raise NotImplementedError
def on_runner_failed(self, runner, e):
print("Runner failed: {} {}".format(e, self))
def before_runner_start(self, runner):
pass
def run(self, **kwargs):
self.generate()
runners = self.get_runners()
if len(runners) > 1:
print("### 分批次执行开始任务, 总共 {}\n".format(len(runners)))
else:
print(">>> 开始执行任务\n")
for i, runner in enumerate(runners, start=1):
if len(runners) > 1:
print(">>> 开始执行第 {} 批任务".format(i))
self.before_runner_start(runner)
try:
cb = runner.run(**kwargs)
self.on_runner_done(runner, cb)
except Exception as e:
self.on_runner_failed(runner, e)
print('\n\n')

View File

@ -0,0 +1,46 @@
- hosts: mysql
gather_facts: no
vars:
ansible_python_interpreter: /usr/local/bin/python
jms_account:
username: root
secret: redhat
jms_asset:
address: 127.0.0.1
port: 3306
account:
username: web1
secret: jumpserver
tasks:
- name: Test MySQL connection
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: version
register: db_info
- name: MySQL version
debug:
var: db_info.version.full
- name: Change MySQL password
community.mysql.mysql_user:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
name: "{{ account.username }}"
password: "{{ account.secret }}"
host: "%"
when: db_info is succeeded
- name: Verify password
community.mysql.mysql_info:
login_user: "{{ account.username }}"
login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
filter: version

View File

@ -1,6 +1,6 @@
id: change_password_mysql
id: change_secret_mysql
name: Change password for MySQL
category: database
type:
- mysql
method: change_password
method: change_secret

View File

@ -0,0 +1,47 @@
- hosts: postgre
gather_facts: no
vars:
ansible_python_interpreter: /usr/local/bin/python
jms_account:
username: postgre
secret: postgre
jms_asset:
address: 127.0.0.1
port: 5432
database: testdb
account:
username: test
secret: jumpserver
tasks:
- name: Test PostgreSQL connection
community.postgresql.postgresql_ping:
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 }}"
register: db_info
- name: Display PostgreSQL version
debug:
var: db_info.version.full
- name: Change PostgreSQL password
community.postgresql.postgresql_user:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
db: "{{ jms_asset.database }}"
name: "{{ account.username }}"
password: "{{ account.secret }}"
when: db_info is succeeded
- name: Verify password
community.postgresql.postgresql_ping:
login_user: "{{ account.username }}"
login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
db: "{{ jms_asset.database }}"

View File

@ -1,6 +1,6 @@
id: change_password_postgresql
id: change_secret_postgresql
name: Change password for PostgreSQL
category: database
type:
- postgresql
method: change_password
method: change_secret

View File

@ -0,0 +1,2 @@
# all base inventory in base/base_inventory.txt
asset_name(ip)_account_username account={"username": "", "password": "xxx"} ...base_inventory_vars

View File

@ -0,0 +1,29 @@
- hosts: demo
tasks:
- name: ping
ping:
#- name: print variables
# debug:
# msg: "Username: {{ account.username }}, Password: {{ account.password }}"
- name: Change password
user:
name: "{{ account.username }}"
password: "{{ account.password | password_hash('des') }}"
update_password: always
when: account.password
- name: Change public key
authorized_key:
user: "{{ account.username }}"
key: "{{ account.public_key }}"
state: present
when: account.public_key
- name: Verify password
ping:
vars:
ansible_user: "{{ account.username }}"
ansible_pass: "{{ account.password }}"
ansible_ssh_connection: paramiko

View File

@ -1,6 +1,6 @@
id: change_password_aix
id: change_secret_aix
name: Change password for AIX
category: host
type:
- aix
method: change_password
method: change_secret

View File

@ -0,0 +1,30 @@
- hosts: demo
gather_facts: no
tasks:
- name: Test privileged account
ping:
#- name: print variables
# debug:
# msg: "Username: {{ account.username }}, Secret: {{ account.secret }}, Secret type: {{ account.secret_type }}"
- name: Change password
user:
name: "{{ account.username }}"
password: "{{ account.secret | password_hash('sha512') }}"
update_password: always
when: account.secret_type == 'password'
- name: Change public key
authorized_key:
user: "{{ account.username }}"
key: "{{ account.public_key }}"
state: present
when: account.public_key
- name: Verify password
ping:
vars:
ansible_user: "{{ account.username }}"
ansible_pass: "{{ account.secret }}"
ansible_ssh_connection: paramiko

View File

@ -1,7 +1,7 @@
id: change_password_linux
id: change_secret_linux
name: Change password for Linux
category: host
type:
- unix
- linux
method: change_password
method: change_secret

View File

@ -0,0 +1,30 @@
- hosts: demo
gather_facts: no
tasks:
- name: ping
ping:
#- name: print variables
# debug:
# msg: "Username: {{ account.username }}, Password: {{ account.password }}"
- name: Change password
user:
name: "{{ account.username }}"
password: "{{ account.password | password_hash('des') }}"
update_password: always
when: account.password
- name: Change public key
authorized_key:
user: "{{ account.username }}"
key: "{{ account.public_key }}"
state: present
when: account.public_key
- name: Verify password
ping:
vars:
ansible_user: "{{ account.username }}"
ansible_pass: "{{ account.password }}"
ansible_ssh_connection: paramiko

View File

@ -1,7 +1,7 @@
id: change_password_local_windows
id: change_secret_local_windows
name: Change password local account for Windows
version: 1
method: change_password
method: change_secret
category: host
type:
- windows

View File

@ -0,0 +1,130 @@
from copy import deepcopy
from collections import defaultdict
import random
import string
from common.utils import lazyproperty, gen_key_pair
from ..base.manager import BasePlaybookManager
from assets.models import ChangeSecretRecord, SecretStrategy
string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_~'
DEFAULT_PASSWORD_LENGTH = 30
DEFAULT_PASSWORD_RULES = {
'length': DEFAULT_PASSWORD_LENGTH,
'symbol_set': string_punctuation
}
class ChangeSecretManager(BasePlaybookManager):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.method_hosts_mapper = defaultdict(list)
self.playbooks = []
self.password_strategy = self.execution.automation.password_strategy
self.ssh_key_strategy = self.execution.automation.ssh_key_strategy
self._password_generated = None
self._ssh_key_generated = None
self.name_recorder_mapper = {} # 做个映射,方便后面处理
@classmethod
def method_type(cls):
return 'change_secret'
@lazyproperty
def related_accounts(self):
pass
@staticmethod
def generate_ssh_key():
private_key, public_key = gen_key_pair()
return private_key
def generate_password(self):
kwargs = self.automation.password_rules or {}
length = int(kwargs.get('length', DEFAULT_PASSWORD_RULES['length']))
symbol_set = kwargs.get('symbol_set')
if symbol_set is None:
symbol_set = DEFAULT_PASSWORD_RULES['symbol_set']
chars = string.ascii_letters + string.digits + symbol_set
password = ''.join([random.choice(chars) for _ in range(length)])
return password
def get_ssh_key(self):
if self.ssh_key_strategy == SecretStrategy.custom:
return self.automation.ssh_key
elif self.ssh_key_strategy == SecretStrategy.random_one:
if not self._ssh_key_generated:
self._ssh_key_generated = self.generate_ssh_key()
return self._ssh_key_generated
else:
self.generate_ssh_key()
def get_password(self):
if self.password_strategy == SecretStrategy.custom:
if not self.automation.password:
raise ValueError("Automation Password must be set")
return self.automation.password
elif self.password_strategy == SecretStrategy.random_one:
if not self._password_generated:
self._password_generated = self.generate_password()
return self._password_generated
else:
self.generate_password()
def get_secret(self, account):
if account.secret_type == 'ssh-key':
secret = self.get_ssh_key()
else:
secret = self.get_password()
if not secret:
raise ValueError("Secret must be set")
return secret
def host_callback(self, host, asset=None, account=None, automation=None, **kwargs):
host = super().host_callback(host, asset=asset, account=account, automation=automation, **kwargs)
if host.get('error'):
return host
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)
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']]
inventory_hosts = []
records = []
for account in accounts:
h = deepcopy(host)
h['name'] += '_' + account.username
new_secret = self.get_secret(account)
recorder = ChangeSecretRecord(
account=account, execution=self.execution,
old_secret=account.secret, new_secret=new_secret,
)
records.append(recorder)
self.name_recorder_mapper[h['name']] = recorder
h['account'] = {
'name': account.name,
'username': account.username,
'secret_type': account.secret_type,
'secret': new_secret,
}
inventory_hosts.append(h)
method_hosts.append(h['name'])
self.method_hosts_mapper[method_attr] = method_hosts
ChangeSecretRecord.objects.bulk_create(records)
return inventory_hosts
def on_runner_done(self, runner, cb):
summary = runner.summary
def on_runner_failed(self, runner, e):
pass

View File

@ -0,0 +1,18 @@
# from .backup.manager import AccountBackupExecutionManager
#
#
from .change_secret.manager import ChangeSecretManager
class ExecutionManager:
manager_type_mapper = {
'change_secret': ChangeSecretManager,
}
def __init__(self, execution):
self.execution = execution
self._runner = self.manager_type_mapper[execution.automation.type](execution)
def run(self, **kwargs):
return self._runner.run(**kwargs)

View File

@ -0,0 +1,28 @@
- hosts: mysql
gather_facts: no
vars:
ansible_python_interpreter: /usr/local/bin/python
jms_account:
username: root
secret: redhat
jms_asset:
address: 127.0.0.1
port: 3306
tasks:
- name: Gather facts 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 }}"
register: db_info
- name: Get info
set_fact:
info:
version: "{{ db_info.version.full }}"
- debug:
var: db_info

View File

@ -0,0 +1,6 @@
id: gather_facts_mysql
name: Gather facts from MySQL
category: database
type:
- mysql
method: gather_facts

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
- hosts: postgre
gather_facts: no
vars:
ansible_python_interpreter: /usr/local/bin/python
jms_account:
username: postgre
secret: postgre
jms_asset:
address: 127.0.0.1
port: 5432
database: testdb
account:
username: test
secret: jumpserver
tasks:
- name: Test PostgreSQL connection
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 }}"
register: db_info
- name: Debug it
debug:
var: db_info

View File

@ -0,0 +1,6 @@
id: gather_facts_postgresql
name: Gather facts for PostgreSQL
category: database
type:
- postgresql
method: gather_facts

View File

@ -6,5 +6,5 @@
password: {{ account.password }}
public_key: {{ account.public_key }}
roles:
- change_password
- change_secret
{% endfor %}

View File

@ -1,8 +1,8 @@
id: change_password_sqlserver
id: gather_facts_sqlserver
name: Change password for SQLServer
version: 1
category: database
type:
- sqlserver
method: change_password
method: gather_facts

View File

@ -0,0 +1,2 @@
# all base inventory in base/base_inventory.txt
asset_name(ip) ...base_inventory_vars

View File

@ -0,0 +1,19 @@
- hosts: website
gather_facts: yes
tasks:
- name: Get info
set_fact:
info:
arch: "{{ ansible_architecture }}"
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 }}"
disk_total: "{{ (ansible_mounts | map(attribute='size_total') | sum / 1024 / 1024 / 1024) | round(2) }}"
- debug:
var: info

View File

@ -0,0 +1,8 @@
id: gather_facts_posix
name: Gather posix facts
category: host
type:
- linux
- windows
- unix
method: gather_facts

View File

@ -0,0 +1,24 @@
- hosts: windows
gather_facts: yes
tasks:
# - name: Gather facts windows
# setup:
# register: facts
#
# - debug:
# var: facts
- 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 }}"
t
- debug:
var: info

View File

@ -0,0 +1,7 @@
id: gather_facts_windows
name: Gather facts windows
version: 1
method: gather_facts
category: host
type:
- windows

View File

@ -0,0 +1,77 @@
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 ..base.manager import BasePlaybookManager
from assets.automations.methods import platform_automation_methods
class GatherFactsManager(BasePlaybookManager):
method_name = 'gather_facts'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.id_method_mapper = {
method['id']: method
for method in platform_automation_methods
if method['method'] == self.method_name
}
self.method_hosts_mapper = defaultdict(list)
self.playbooks = []
def inventory_kwargs(self):
return {
}
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
)

View File

@ -1,5 +1,6 @@
import os
import yaml
import json
from functools import partial
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
@ -62,4 +63,4 @@ platform_automation_methods = get_platform_automation_methods()
if __name__ == '__main__':
print(get_platform_automation_methods())
print(json.dumps(platform_automation_methods, indent=4))

View File

View File

@ -0,0 +1,20 @@
- hosts: mysql
gather_facts: no
vars:
ansible_python_interpreter: /usr/local/bin/python
jms_account:
username: root
password: redhat
jms_asset:
address: 127.0.0.1
port: 3306
tasks:
- name: Test MySQL connection
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: version
register: db_info

View File

@ -0,0 +1,6 @@
id: mysql_ping
name: Ping MySQL
category: database
type:
- mysql
method: ping

View File

@ -0,0 +1,23 @@
- hosts: postgre
gather_facts: no
vars:
ansible_python_interpreter: /usr/local/bin/python
jms_account:
username: postgre
secret: postgre
jms_asset:
address: 127.0.0.1
port: 5432
database: testdb
account:
username: test
secret: jumpserver
tasks:
- name: Test PostgreSQL connection
community.postgresql.postgresql_ping:
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 }}"

View File

@ -0,0 +1,6 @@
id: ping_postgresql
name: Ping PostgreSQL
category: database
type:
- postgresql
method: ping

View File

@ -0,0 +1,2 @@
# all base inventory in base/base_inventory.txt
asset_name(ip)_account_username account={"username": "", "password": "xxx"} ...base_inventory_vars

View File

@ -0,0 +1,5 @@
- hosts: demo
gather_facts: no
tasks:
- name: Posix ping
ping:

View File

@ -0,0 +1,8 @@
id: posix_ping
name: Posix ping
category: host
type:
- linux
- windows
- unix
method: ping

View File

@ -0,0 +1,5 @@
- hosts: windows
gather_facts: no
tasks:
- name: Windows ping
win_ping:

View File

@ -0,0 +1,7 @@
id: win_ping
name: Windows ping
version: 1
method: change_secret
category: host
type:
- windows

View File

@ -0,0 +1,75 @@
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 ..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
)

View File

@ -24,7 +24,7 @@ class CloudTypes(BaseType):
'ansible_config': {},
'gather_facts_enabled': False,
'verify_account_enabled': False,
'change_password_enabled': False,
'change_secret_enabled': False,
'create_account_enabled': False,
'gather_accounts_enabled': False,
}

View File

@ -32,7 +32,7 @@ class DatabaseTypes(BaseType):
'gather_facts_enabled': True,
'gather_accounts_enabled': True,
'verify_account_enabled': True,
'change_password_enabled': True,
'change_secret_enabled': True,
'create_account_enabled': True,
}
}

View File

@ -39,7 +39,7 @@ class DeviceTypes(BaseType):
'gather_facts_enabled': False,
'gather_accounts_enabled': False,
'verify_account_enabled': False,
'change_password_enabled': False,
'change_secret_enabled': False,
'create_account_enabled': False,
}
}

View File

@ -48,12 +48,12 @@ class HostTypes(BaseType):
'gather_facts_enabled': True,
'gather_accounts_enabled': True,
'verify_account_enabled': True,
'change_password_enabled': True,
'change_secret_enabled': True,
'create_account_enabled': True,
},
cls.WINDOWS: {
'ansible_config': {
'ansible_shell_type': 'powershell',
'ansible_shell_type': 'cmd',
'ansible_connection': 'ssh',
},
},
@ -71,7 +71,7 @@ class HostTypes(BaseType):
{'name': 'BSD'},
{'name': 'AIX', 'automation': {
'create_account_method': 'create_account_aix',
'change_password_method': 'change_password_aix'
'change_secret_method': 'change_secret_aix'
}},
],
cls.WINDOWS: [

View File

@ -38,7 +38,7 @@ class AllTypes(ChoicesMixin):
@classmethod
def set_automation_methods(cls, category, tp, constraints):
from assets.playbooks import filter_platform_methods
from assets.automations import filter_platform_methods
automation = constraints.get('automation', {})
automation_methods = {}
for item, enabled in automation.items():

View File

@ -22,7 +22,7 @@ class WebTypes(BaseType):
'*': {
'gather_facts_enabled': False,
'verify_account_enabled': False,
'change_password_enabled': False,
'change_secret_enabled': False,
'create_account_enabled': False,
'gather_accounts_enabled': False,
}

View File

@ -3,8 +3,9 @@
from django.db.models import Q
from rest_framework import filters
from rest_framework.compat import coreapi, coreschema
from django_filters import rest_framework as drf_filters
from common.drf.filters import BaseFilterSet, UUIDInFilter
from common.drf.filters import BaseFilterSet
from assets.utils import is_query_node_all_assets, get_node_from_request
from .models import Label, Node, Account
@ -157,15 +158,16 @@ class IpInFilterBackend(filters.BaseFilterBackend):
class AccountFilterSet(BaseFilterSet):
from django_filters import rest_framework as filters
ip = filters.CharFilter(field_name='address', lookup_expr='exact')
hostname = filters.CharFilter(field_name='name', lookup_expr='exact')
username = filters.CharFilter(field_name="username", lookup_expr='exact')
address = filters.CharFilter(field_name="asset__address", lookup_expr='exact')
assets = UUIDInFilter(field_name='asset_id', lookup_expr='in')
nodes = UUIDInFilter(method='filter_nodes')
ip = drf_filters.CharFilter(field_name='address', lookup_expr='exact')
hostname = drf_filters.CharFilter(field_name='name', lookup_expr='exact')
username = drf_filters.CharFilter(field_name="username", lookup_expr='exact')
address = drf_filters.CharFilter(field_name="asset__address", lookup_expr='exact')
asset = drf_filters.CharFilter(field_name="asset_id", lookup_expr='exact')
assets = drf_filters.CharFilter(field_name='asset_id', lookup_expr='in')
nodes = drf_filters.CharFilter(method='filter_nodes')
def filter_nodes(self, queryset, name, value):
@staticmethod
def filter_nodes(queryset, name, value):
nodes = Node.objects.filter(id__in=value)
if not nodes:
return queryset
@ -179,6 +181,4 @@ class AccountFilterSet(BaseFilterSet):
class Meta:
model = Account
fields = [
'asset', 'id'
]
fields = ['id']

View File

@ -33,8 +33,8 @@ class Migration(migrations.Migration):
('gather_facts_method', models.TextField(blank=True, max_length=32, null=True, verbose_name='Gather facts method')),
('create_account_enabled', models.BooleanField(default=False, verbose_name='Create account enabled')),
('create_account_method', models.TextField(blank=True, max_length=32, null=True, verbose_name='Create account method')),
('change_password_enabled', models.BooleanField(default=False, verbose_name='Change password enabled')),
('change_password_method', models.TextField(blank=True, max_length=32, null=True, verbose_name='Change password method')),
('change_secret_enabled', models.BooleanField(default=False, verbose_name='Change password enabled')),
('change_secret_method', models.TextField(blank=True, max_length=32, null=True, verbose_name='Change password method')),
('verify_account_enabled', models.BooleanField(default=False, verbose_name='Verify account enabled')),
('verify_account_method', models.TextField(blank=True, max_length=32, null=True, verbose_name='Verify account method')),
('gather_accounts_enabled', models.BooleanField(default=False, verbose_name='Gather facts enabled')),

View File

@ -0,0 +1,117 @@
# Generated by Django 3.2.14 on 2022-10-10 01:59
import common.db.fields
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('assets', '0107_account_history'),
]
operations = [
migrations.CreateModel(
name='BaseAutomation',
fields=[
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('name', models.CharField(max_length=128, verbose_name='Name')),
('is_periodic', models.BooleanField(default=False)),
('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')),
('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')),
('accounts', models.JSONField(default=list, verbose_name='Accounts')),
('type', models.CharField(max_length=16, verbose_name='Type')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('assets', models.ManyToManyField(blank=True, to='assets.Asset', verbose_name='Assets')),
('nodes', models.ManyToManyField(blank=True, to='assets.Node', verbose_name='Nodes')),
],
options={
'verbose_name': 'Automation plan',
'unique_together': {('org_id', 'name')},
},
),
migrations.AddField(
model_name='label',
name='created_by',
field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by'),
),
migrations.AddField(
model_name='label',
name='date_updated',
field=models.DateTimeField(auto_now=True, verbose_name='Date updated'),
),
migrations.AddField(
model_name='label',
name='updated_by',
field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by'),
),
migrations.CreateModel(
name='DiscoveryAutomation',
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': 'Discovery strategy',
},
bases=('assets.baseautomation',),
),
migrations.CreateModel(
name='ReconcileAutomation',
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': 'Reconcile strategy',
},
bases=('assets.baseautomation',),
),
migrations.CreateModel(
name='VerifyAutomation',
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': 'Verify strategy',
},
bases=('assets.baseautomation',),
),
migrations.CreateModel(
name='AutomationExecution',
fields=[
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('status', models.CharField(default='pending', max_length=16)),
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
('date_start', models.DateTimeField(db_index=True, null=True, verbose_name='Date start')),
('date_finished', models.DateTimeField(null=True, verbose_name='Date finished')),
('snapshot', common.db.fields.EncryptJsonDictTextField(blank=True, default=dict, null=True, verbose_name='Automation snapshot')),
('trigger', models.CharField(choices=[('manual', 'Manual trigger'), ('timing', 'Timing trigger')], default='manual', max_length=128, verbose_name='Trigger mode')),
('automation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='executions', to='assets.baseautomation', verbose_name='Automation strategy')),
],
options={
'verbose_name': 'Automation strategy execution',
},
),
migrations.CreateModel(
name='ChangePasswordAutomation',
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')),
('password', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('recipients', models.ManyToManyField(blank=True, related_name='recipients_change_auth_strategy', to=settings.AUTH_USER_MODEL, verbose_name='Recipient')),
],
options={
'verbose_name': 'Change auth strategy',
},
bases=('assets.baseautomation',),
),
]

View File

@ -0,0 +1,83 @@
# Generated by Django 3.2.14 on 2022-10-13 09:51
import common.db.fields
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('assets', '0108_migrate_automation'),
]
operations = [
migrations.RenameModel(
old_name='ChangePasswordAutomation',
new_name='ChangeSecretAutomation',
),
migrations.AddField(
model_name='baseautomation',
name='is_active',
field=models.BooleanField(default=True, verbose_name='Is active'),
),
migrations.AddField(
model_name='changesecretautomation',
name='password_rules',
field=models.JSONField(default=dict, verbose_name='Password rules'),
),
migrations.AddField(
model_name='changesecretautomation',
name='password_strategy',
field=models.CharField(choices=[('specific', 'Specific'), ('random_one', 'All assets use the same random password'), ('random_all', 'All assets use different random password')], default='random_one', max_length=16, verbose_name='Password strategy'),
),
migrations.AddField(
model_name='changesecretautomation',
name='secret_types',
field=models.JSONField(default=list, verbose_name='Secret types'),
),
migrations.AddField(
model_name='changesecretautomation',
name='ssh_key',
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH key'),
),
migrations.AddField(
model_name='changesecretautomation',
name='ssh_key_change_strategy',
field=models.CharField(choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), ('set_jms', 'Replace (The key generated by JumpServer) ')], default='add', max_length=16, verbose_name='SSH key strategy'),
),
migrations.AddField(
model_name='changesecretautomation',
name='ssh_key_strategy',
field=models.CharField(choices=[('specific', 'Specific'), ('random_one', 'All assets use the same random password'), ('random_all', 'All assets use different random password')], default='random_one', max_length=16),
),
migrations.AlterField(
model_name='changesecretautomation',
name='recipients',
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Recipient'),
),
migrations.CreateModel(
name='ChangeSecretRecord',
fields=[
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('old_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Old secret')),
('new_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('date_started', models.DateTimeField(blank=True, null=True, verbose_name='Date started')),
('date_finished', models.DateTimeField(blank=True, null=True, verbose_name='Date finished')),
('status', models.CharField(default='pending', max_length=16)),
('error', models.TextField(blank=True, null=True, verbose_name='Error')),
('account', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.account')),
('execution', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.automationexecution')),
],
options={
'verbose_name': 'Change secret',
},
),
]

View File

@ -10,6 +10,7 @@ from .gathered_user import *
from .favorite_asset import *
from .account import *
from .backup import *
from .automations import *
from ._user import *
# 废弃以下
# from ._authbook import *

View File

@ -4,6 +4,7 @@
import logging
import uuid
from collections import defaultdict
from django.db import models
from django.db.models import Q
@ -41,6 +42,12 @@ class AssetQuerySet(models.QuerySet):
def has_protocol(self, name):
return self.filter(protocols__contains=name)
def group_by_platform(self) -> dict:
groups = defaultdict(list)
for asset in self.all():
groups[asset.platform].append(asset)
return groups
class NodesRelationMixin:
NODES_CACHE_KEY = 'ASSET_NODES_{}'
@ -126,6 +133,22 @@ class Asset(AbsConnectivity, NodesRelationMixin, JMSOrgBaseModel):
names.append(n.name + ':' + n.value)
return names
@lazyproperty
def primary_protocol(self):
return self.protocols.first()
@lazyproperty
def protocol(self):
if not self.primary_protocol:
return 'none'
return self.primary_protocol.name
@lazyproperty
def port(self):
if not self.primary_protocol:
return 0
return self.primary_protocol.port
@property
def protocols_as_list(self):
return [{'name': p.name, 'port': p.port} for p in self.protocols.all()]

View File

@ -0,0 +1,4 @@
from .change_secret import *
from .account_discovery import *
from .account_reconcile import *
from .account_verify import *

View File

@ -1,12 +1,13 @@
from django.utils.translation import ugettext_lazy as _
from ops.const import StrategyChoice
from .common import AutomationStrategy
from ops.ansible.runner import PlaybookRunner
from .base import BaseAutomation
class CollectStrategy(AutomationStrategy):
class DiscoveryAutomation(BaseAutomation):
class Meta:
verbose_name = _("Collect strategy")
verbose_name = _("Discovery strategy")
def to_attr_json(self):
attr_json = super().to_attr_json()

View File

@ -1,12 +1,12 @@
from django.utils.translation import ugettext_lazy as _
from ops.const import StrategyChoice
from .common import AutomationStrategy
from .base import BaseAutomation
class PushStrategy(AutomationStrategy):
class ReconcileAutomation(BaseAutomation):
class Meta:
verbose_name = _("Push strategy")
verbose_name = _("Reconcile strategy")
def to_attr_json(self):
attr_json = super().to_attr_json()

View File

@ -1,10 +1,10 @@
from django.utils.translation import ugettext_lazy as _
from ops.const import StrategyChoice
from .common import AutomationStrategy
from .base import BaseAutomation
class VerifyStrategy(AutomationStrategy):
class VerifyAutomation(BaseAutomation):
class Meta:
verbose_name = _("Verify strategy")

View File

@ -0,0 +1,99 @@
import uuid
from celery import current_task
from django.db import models
from django.utils.translation import ugettext_lazy as _
from common.const.choices import Trigger
from common.db.fields import EncryptJsonDictTextField
from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel
from ops.mixin import PeriodTaskModelMixin
from ops.tasks import execute_automation_strategy
from assets.models import Node, Asset
class BaseAutomation(JMSOrgBaseModel, PeriodTaskModelMixin):
accounts = models.JSONField(default=list, verbose_name=_("Accounts"))
nodes = models.ManyToManyField(
'assets.Node', blank=True, verbose_name=_("Nodes")
)
assets = models.ManyToManyField(
'assets.Asset', blank=True, verbose_name=_("Assets")
)
type = models.CharField(max_length=16, verbose_name=_('Type'))
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
comment = models.TextField(blank=True, verbose_name=_('Comment'))
def __str__(self):
return self.name + '@' + str(self.created_by)
def get_all_assets(self):
nodes = self.nodes.all()
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
direct_asset_ids = self.assets.all().values_list('id', flat=True)
asset_ids = set(list(direct_asset_ids) + list(node_asset_ids))
return Asset.objects.filter(id__in=asset_ids)
def all_assets_group_by_platform(self):
assets = self.get_all_assets().prefetch_related('platform')
return assets.group_by_platform()
def get_register_task(self):
name = "automation_strategy_period_{}".format(str(self.id)[:8])
task = execute_automation_strategy.name
args = (str(self.id), Trigger.timing)
kwargs = {}
return name, task, args, kwargs
def to_attr_json(self):
return {
'name': self.name,
'accounts': self.accounts,
'assets': list(self.assets.all().values_list('id', flat=True)),
'nodes': list(self.assets.all().values_list('id', flat=True)),
}
def execute(self, trigger=Trigger.manual):
try:
eid = current_task.request.id
except AttributeError:
eid = str(uuid.uuid4())
execution = self.executions.create(
id=eid, trigger=trigger,
)
return execution.start()
class Meta:
unique_together = [('org_id', 'name')]
verbose_name = _("Automation plan")
class AutomationExecution(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
automation = models.ForeignKey(
'BaseAutomation', related_name='executions', on_delete=models.CASCADE,
verbose_name=_('Automation strategy')
)
status = models.CharField(max_length=16, default='pending')
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created'))
date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True)
date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished"))
snapshot = EncryptJsonDictTextField(
default=dict, blank=True, null=True, verbose_name=_('Automation snapshot')
)
trigger = models.CharField(
max_length=128, default=Trigger.manual, choices=Trigger.choices,
verbose_name=_('Trigger mode')
)
class Meta:
verbose_name = _('Automation strategy execution')
@property
def manager_type(self):
return self.snapshot['type']
def start(self):
from assets.automations.endpoint import ExecutionManager
manager = ExecutionManager(execution=self)
return manager.run()

View File

@ -0,0 +1,59 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from common.db import fields
from common.db.models import JMSBaseModel
from .base import BaseAutomation
__all__ = ['ChangeSecretAutomation', 'ChangeSecretRecord', 'SecretStrategy']
class SecretStrategy(models.TextChoices):
custom = 'specific', _('Specific')
random_one = 'random_one', _('All assets use the same random password')
random_all = 'random_all', _('All assets use different random password')
class SSHKeyStrategy(models.TextChoices):
add = 'add', _('Append SSH KEY')
set = 'set', _('Empty and append SSH KEY')
set_jms = 'set_jms', _('Replace (The key generated by JumpServer) ')
class ChangeSecretAutomation(BaseAutomation):
secret_types = models.JSONField(default=list, verbose_name=_('Secret types'))
password_strategy = models.CharField(choices=SecretStrategy.choices, max_length=16,
default=SecretStrategy.random_one, verbose_name=_('Password strategy'))
password = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
password_rules = models.JSONField(default=dict, verbose_name=_('Password rules'))
ssh_key_strategy = models.CharField(choices=SecretStrategy.choices, default=SecretStrategy.random_one, max_length=16)
ssh_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH key'))
ssh_key_change_strategy = models.CharField(choices=SSHKeyStrategy.choices, max_length=16,
default=SSHKeyStrategy.add, verbose_name=_('SSH key strategy'))
recipients = models.ManyToManyField('users.User', blank=True, verbose_name=_("Recipient"))
def save(self, *args, **kwargs):
self.type = 'change_secret'
super().save(*args, **kwargs)
class Meta:
verbose_name = _("Change auth strategy")
class ChangeSecretRecord(JMSBaseModel):
execution = models.ForeignKey('assets.AutomationExecution', on_delete=models.CASCADE)
account = models.ForeignKey('assets.Account', on_delete=models.CASCADE, null=True)
old_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Old secret'))
new_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
date_started = models.DateTimeField(blank=True, null=True, verbose_name=_('Date started'))
date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('Date finished'))
status = models.CharField(max_length=16, default='pending')
error = models.TextField(blank=True, null=True, verbose_name=_('Error'))
class Meta:
verbose_name = _("Change secret")
def __str__(self):
return self.account.__str__()

View File

@ -76,6 +76,15 @@ class BaseAccount(OrgModelMixin):
def has_secret(self):
return bool(self.secret)
@property
def password(self):
return self.secret
@password.setter
def password(self, value):
self.secret = value
self.secret_type = 'password'
@property
def private_key(self):
if self.secret_type == self.SecretType.ssh_key:
@ -91,15 +100,6 @@ class BaseAccount(OrgModelMixin):
self.secret = value
self.secret_type = 'private_key'
@property
def password(self):
return self.secret
@password.setter
def password(self, value):
self.secret = value
self.secret_type = 'password'
@property
def ssh_key_fingerprint(self):
if self.public_key:
@ -125,8 +125,8 @@ class BaseAccount(OrgModelMixin):
return None
@property
def private_key_file(self):
if not self.private_key_obj:
def private_key_path(self):
if not self.secret_type != 'ssh_key' or not self.secret:
return None
project_dir = settings.PROJECT_DIR
tmp_dir = os.path.join(project_dir, 'tmp')

View File

@ -40,6 +40,9 @@ class Domain(OrgModelMixin):
def gateways(self):
return self.gateway_set.filter(is_active=True)
def select_gateway(self):
return self.random_gateway()
def random_gateway(self):
gateways = [gw for gw in self.gateways if gw.is_connective]
if gateways:

View File

@ -1,29 +1,25 @@
# -*- coding: utf-8 -*-
#
import uuid
from django.db import models
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.models import OrgModelMixin
from orgs.mixins.models import JMSOrgBaseModel
class Label(OrgModelMixin):
class Label(JMSOrgBaseModel):
SYSTEM_CATEGORY = "S"
USER_CATEGORY = "U"
CATEGORY_CHOICES = (
("S", _("System")),
("U", _("User"))
)
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_("Name"))
value = models.CharField(max_length=128, verbose_name=_("Value"))
category = models.CharField(max_length=128, choices=CATEGORY_CHOICES,
default=USER_CATEGORY, verbose_name=_("Category"))
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
comment = models.TextField(blank=True, null=True, verbose_name=_("Comment"))
date_created = models.DateTimeField(
auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')
)
@classmethod
def get_queryset_group_by_name(cls):

View File

@ -39,8 +39,8 @@ class PlatformAutomation(models.Model):
gather_facts_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Gather facts method"))
create_account_enabled = models.BooleanField(default=False, verbose_name=_("Create account enabled"))
create_account_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Create account method"))
change_password_enabled = models.BooleanField(default=False, verbose_name=_("Change password enabled"))
change_password_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Change password method"))
change_secret_enabled = models.BooleanField(default=False, verbose_name=_("Change password enabled"))
change_secret_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Change password method"))
verify_account_enabled = models.BooleanField(default=False, verbose_name=_("Verify account enabled"))
verify_account_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Verify account method"))
gather_accounts_enabled = models.BooleanField(default=False, verbose_name=_("Gather facts enabled"))

View File

@ -33,7 +33,7 @@ def update_internal_platforms(platform_model):
{
'name': 'AIX', 'category': 'host', 'type': 'unix',
'create_account_method': 'create_account_aix',
'change_password_method': 'change_password_aix',
'change_secret_method': 'change_secret_aix',
},
{'name': 'Windows', 'category': 'host', 'type': 'windows'},
{

View File

@ -1,33 +0,0 @@
import os
import time
import shutil
from typing import List
from django.conf import settings
from assets.models import Asset
class BaseRunner:
src_filepath: str
def __init__(self, assets: List[Asset], strategy):
self.assets = assets
self.strategy = strategy
self.temp_folder = self.temp_folder_path()
@staticmethod
def temp_folder_path():
project_dir = settings.PROJECT_DIR
tmp_dir = os.path.join(project_dir, 'tmp')
filepath = os.path.join(tmp_dir, str(time.time()))
return filepath
def del_temp_folder(self):
shutil.rmtree(self.temp_folder)
def generate_temp_playbook(self):
src = self.src_filepath
dst = os.path.join(self.temp_folder, self.strategy)
shutil.copytree(src, dst)
return dst

View File

@ -1,47 +0,0 @@
import os
import tempfile
import shutil
from typing import List
from django.conf import settings
from assets.models import Asset
class BasePlaybookGenerator:
def __init__(self, assets: list[Asset], strategy, ansible_connection='ssh'):
self.assets = assets
self.strategy = strategy
self.playbook_dir = self.temp_folder_path()
def generate(self):
self.prepare_playbook_dir()
self.generate_inventory()
self.generate_playbook()
def prepare_playbook_dir(self):
pass
def generate_inventory(self):
pass
def generate_playbook(self):
pass
@property
def base_dir(self):
tmp_dir = os.path.join(settings.PROJECT_DIR, 'tmp')
path = os.path.join(tmp_dir, self.strategy)
return path
def temp_folder_path(self):
return tempfile.mkdtemp(dir=self.base_dir)
def del_temp_folder(self):
shutil.rmtree(self.playbook_dir)
def generate_temp_playbook(self):
src = self.src_filepath
dst = os.path.join(self.temp_folder, self.strategy)
shutil.copytree(src, dst)
return dst

View File

@ -1,10 +0,0 @@
{% for account in accounts %}
- hosts: {{ account.asset.name }}
vars:
account:
username: {{ account.username }}
password: {{ account.password }}
public_key: {{ account.public_key }}
roles:
- change_password
{% endfor %}

View File

@ -1,6 +0,0 @@
id: change_password_oracle
name: Change password for Oracle
method: change_password
category: database
type:
- oracle

View File

@ -1,27 +0,0 @@
- name: ping
ping:
#- name: print variables
# debug:
# msg: "Username: {{ account.username }}, Password: {{ account.password }}"
- name: Change password
user:
name: "{{ account.username }}"
password: "{{ account.password | password_hash('des') }}"
update_password: always
when: account.password
- name: Change public key
authorized_key:
user: "{{ account.username }}"
key: "{{ account.public_key }}"
state: present
when: account.public_key
- name: Verify password
ping:
vars:
ansible_user: "{{ account.username }}"
ansible_pass: "{{ account.password }}"
ansible_ssh_connection: paramiko

View File

@ -1,10 +0,0 @@
{% for account in accounts %}
- hosts: {{ account.asset.name }}
vars:
account:
username: {{ account.username }}
password: {{ account.password }}
public_key: {{ account.public_key }}
roles:
- change_password
{% endfor %}

View File

@ -1,27 +0,0 @@
- name: ping
ping:
#- name: print variables
# debug:
# msg: "Username: {{ account.username }}, Password: {{ account.password }}"
- name: Change password
user:
name: "{{ account.username }}"
password: "{{ account.password | password_hash('des') }}"
update_password: always
when: account.password
- name: Change public key
authorized_key:
user: "{{ account.username }}"
key: "{{ account.public_key }}"
state: present
when: account.public_key
- name: Verify password
ping:
vars:
ansible_user: "{{ account.username }}"
ansible_pass: "{{ account.password }}"
ansible_ssh_connection: paramiko

View File

@ -1,10 +0,0 @@
{% for account in accounts %}
- hosts: {{ account.asset.name }}
vars:
account:
username: {{ account.username }}
password: {{ account.password }}
public_key: {{ account.public_key }}
roles:
- change_password
{% endfor %}

View File

@ -1,27 +0,0 @@
- name: ping
ping:
#- name: print variables
# debug:
# msg: "Username: {{ account.username }}, Password: {{ account.password }}"
- name: Change password
user:
name: "{{ account.username }}"
password: "{{ account.password | password_hash('des') }}"
update_password: always
when: account.password
- name: Change public key
authorized_key:
user: "{{ account.username }}"
key: "{{ account.public_key }}"
state: present
when: account.public_key
- name: Verify password
ping:
vars:
ansible_user: "{{ account.username }}"
ansible_pass: "{{ account.password }}"
ansible_ssh_connection: paramiko

View File

@ -1,10 +0,0 @@
{% for account in accounts %}
- hosts: {{ account.asset.name }}
vars:
account:
username: {{ account.username }}
password: {{ account.password }}
public_key: {{ account.public_key }}
roles:
- change_password
{% endfor %}

View File

@ -1,27 +0,0 @@
- name: ping
ping:
#- name: print variables
# debug:
# msg: "Username: {{ account.username }}, Password: {{ account.password }}"
- name: Change password
user:
name: "{{ account.username }}"
password: "{{ account.password | password_hash('des') }}"
update_password: always
when: account.password
- name: Change public key
authorized_key:
user: "{{ account.username }}"
key: "{{ account.public_key }}"
state: present
when: account.public_key
- name: Verify password
ping:
vars:
ansible_user: "{{ account.username }}"
ansible_pass: "{{ account.password }}"
ansible_ssh_connection: paramiko

View File

@ -1,8 +0,0 @@
- hosts: all
vars:
account:
username: {{ account.username }}
password: {{ account.password }}
public_key: {{ account.public_key }}
roles:
- change_password

View File

@ -1,23 +0,0 @@
- name: Check connection
ping:
- name: Change password
user:
name: "{{ account.username }}"
password: "{{ account.password | password_hash('sha512') }}"
update_password: always
when: account.password
- name: Change public key
authorized_key:
user: "{{ account.username }}"
key: "{{ account.public_key }}"
state: present
when: account.public_key
- name: Verify password
ping:
vars:
ansible_user: "{{ account.username }}"
ansible_pass: "{{ account.password }}"
ansible_ssh_connection: paramiko

View File

@ -1,10 +0,0 @@
{% for account in accounts %}
- hosts: {{ account.asset.name }}
vars:
account:
username: {{ account.username }}
password: {{ account.password }}
public_key: {{ account.public_key }}
roles:
- change_password
{% endfor %}

View File

@ -1,27 +0,0 @@
- name: ping
ping:
#- name: print variables
# debug:
# msg: "Username: {{ account.username }}, Password: {{ account.password }}"
- name: Change password
user:
name: "{{ account.username }}"
password: "{{ account.password | password_hash('des') }}"
update_password: always
when: account.password
- name: Change public key
authorized_key:
user: "{{ account.username }}"
key: "{{ account.public_key }}"
state: present
when: account.public_key
- name: Verify password
ping:
vars:
ansible_user: "{{ account.username }}"
ansible_pass: "{{ account.password }}"
ansible_ssh_connection: paramiko

View File

@ -1,106 +0,0 @@
import os
import yaml
import jinja2
from typing import List
from django.conf import settings
from assets.models import Asset
from .base import BaseGeneratePlaybook
class GenerateChangePasswordPlaybook(BaseGeneratePlaybook):
def __init__(
self, assets: List[Asset], strategy, usernames, password='',
private_key='', public_key='', key_strategy=''
):
super().__init__(assets, strategy)
self.password = password
self.public_key = public_key
self.private_key = private_key
self.key_strategy = key_strategy
self.relation_asset_map = self.get_username_relation_asset_map(usernames)
def get_username_relation_asset_map(self, usernames):
# TODO 没特权用户的资产 要考虑网关
complete_map = {
asset: list(asset.accounts.value_list('username', flat=True))
for asset in self.assets
}
if '*' in usernames:
return complete_map
relation_map = {}
for asset, usernames in complete_map.items():
usernames = list(set(usernames) & set(usernames))
if not usernames:
continue
relation_map[asset] = list(set(usernames) & set(usernames))
return relation_map
@property
def src_filepath(self):
return os.path.join(
settings.BASE_DIR, 'assets', 'playbooks', 'strategy',
'change_password', 'roles', self.strategy
)
def generate_hosts(self):
host_pathname = os.path.join(self.temp_folder, 'hosts')
with open(host_pathname, 'w', encoding='utf8') as f:
for asset in self.relation_asset_map.keys():
f.write(f'{asset.name}\n')
def generate_host_vars(self):
host_vars_pathname = os.path.join(self.temp_folder, 'hosts', 'host_vars')
os.makedirs(host_vars_pathname, exist_ok=True)
for asset, usernames in self.relation_asset_map.items():
host_vars = {
'ansible_host': asset.get_target_ip(),
'ansible_port': asset.get_target_ssh_port(), # TODO 需要根绝协议取端口号
'ansible_user': asset.admin_user.username,
'ansible_pass': asset.admin_user.username,
'usernames': usernames,
}
pathname = os.path.join(host_vars_pathname, f'{asset.name}.yml')
with open(pathname, 'w', encoding='utf8') as f:
f.write(yaml.dump(host_vars, allow_unicode=True))
def generate_secret_key_files(self):
if not self.private_key and not self.public_key:
return
file_pathname = os.path.join(self.temp_folder, self.strategy, 'files')
public_pathname = os.path.join(file_pathname, 'id_rsa.pub')
private_pathname = os.path.join(file_pathname, 'id_rsa')
os.makedirs(file_pathname, exist_ok=True)
with open(public_pathname, 'w', encoding='utf8') as f:
f.write(self.public_key)
with open(private_pathname, 'w', encoding='utf8') as f:
f.write(self.private_key)
def generate_role_main(self):
task_main_pathname = os.path.join(self.temp_folder, 'main.yaml')
context = {
'password': self.password,
'key_strategy': self.key_strategy,
'private_key_file': 'id_rsa' if self.private_key else '',
'exclusive': 'no' if self.key_strategy == 'all' else 'yes',
'jms_key': self.public_key.split()[2].strip() if self.public_key else '',
}
with open(task_main_pathname, 'r+', encoding='utf8') as f:
string_var = f.read()
f.seek(0, 0)
response = jinja2.Template(string_var).render(context)
results = yaml.safe_load(response)
f.write(yaml.dump(results, allow_unicode=True))
def execute(self):
self.generate_temp_playbook()
self.generate_hosts()
self.generate_host_vars()
self.generate_secret_key_files()
self.generate_role_main()

View File

@ -1,86 +0,0 @@
import os
import yaml
from typing import List
from django.conf import settings
from assets.models import Asset
from .base import BaseGeneratePlaybook
class GenerateVerifyPlaybook(BaseGeneratePlaybook):
def __init__(
self, assets: List[Asset], strategy, usernames
):
super().__init__(assets, strategy)
self.relation_asset_map = self.get_account_relation_asset_map(usernames)
def get_account_relation_asset_map(self, usernames):
# TODO 没特权用户的资产 要考虑网关
complete_map = {
asset: list(asset.accounts.all())
for asset in self.assets
}
if '*' in usernames:
return complete_map
relation_map = {}
for asset, accounts in complete_map.items():
account_map = {account.username: account for account in accounts}
accounts = [account_map[i] for i in (set(usernames) & set(account_map))]
if not accounts:
continue
relation_map[asset] = accounts
return relation_map
@property
def src_filepath(self):
return os.path.join(
settings.BASE_DIR, 'assets', 'playbooks', 'strategy',
'verify', 'roles', self.strategy
)
def generate_hosts(self):
host_pathname = os.path.join(self.temp_folder, 'hosts')
with open(host_pathname, 'w', encoding='utf8') as f:
for asset in self.relation_asset_map.keys():
f.write(f'{asset.name}\n')
def generate_host_vars(self):
host_vars_pathname = os.path.join(self.temp_folder, 'hosts', 'host_vars')
os.makedirs(host_vars_pathname, exist_ok=True)
for asset, accounts in self.relation_asset_map.items():
account_info = []
for account in accounts:
private_key_filename = f'{asset.name}_{account.username}' if account.private_key else ''
account_info.append({
'username': account.username,
'password': account.password,
'private_key_filename': private_key_filename,
})
host_vars = {
'ansible_host': asset.get_target_ip(),
'ansible_port': asset.get_target_ssh_port(), # TODO 需要根绝协议取端口号
'account_info': account_info,
}
pathname = os.path.join(host_vars_pathname, f'{asset.name}.yml')
with open(pathname, 'w', encoding='utf8') as f:
f.write(yaml.dump(host_vars, allow_unicode=True))
def generate_secret_key_files(self):
file_pathname = os.path.join(self.temp_folder, self.strategy, 'files')
os.makedirs(file_pathname, exist_ok=True)
for asset, accounts in self.relation_asset_map.items():
for account in accounts:
if account.private_key:
path_name = os.path.join(file_pathname, f'{asset.name}_{account.username}')
with open(path_name, 'w', encoding='utf8') as f:
f.write(account.private_key)
def execute(self):
self.generate_temp_playbook()
self.generate_hosts()
self.generate_host_vars()
self.generate_secret_key_files()
# self.generate_role_main() # TODO Linux 暂时不需要

View File

@ -1,13 +0,0 @@
- hosts: centos
gather_facts: no
vars:
account:
username: web
password: test123
tasks:
- name: Verify password
ping:
vars:
ansible_user: "{{ account.username }}"
ansible_pass: "{{ account.password }}"

View File

@ -1,10 +0,0 @@
id: ansible_posix_ping
name: Ansible posix ping
description: Ansible ping
category: host
type:
- linux
- unix
- macos
- bsd
method: verify_account

View File

@ -1,13 +0,0 @@
- hosts: centos
gather_facts: no
vars:
account:
username: web
password: test123
tasks:
- name: Verify password
win_ping:
vars:
ansible_user: "{{ account.username }}"
ansible_pass: "{{ account.password }}"

View File

@ -1,6 +0,0 @@
id: ansible_win_ping
name: Ansible win ping
category: host
type:
- windows
method: verify_account

View File

@ -1,12 +0,0 @@
- hosts: all
vars:
connection_type: ssh
password:
value: {{ password }}
public_key:
value: {{ jms_key }}
exclusive: {{ exclusive }}
key_strategy: {{ key_strategy }}
private_key_file: {{ private_key_file }}
roles:
- linux

View File

@ -1,36 +0,0 @@
- name: Check connection
ping:
- name: Change password
user:
name: "{{ item }}"
password: "{{ password.value | password_hash('sha512') }}"
update_password: always
with_items: "{{ usernames }}"
when: "{{ password.value }}"
- name: Change public key
authorized_key:
user: "{{ item }}"
key: "{{ lookup('file', id_rsa.pub) }}"
state: present
exclusive: "{{ public_key.exclusive }}"
with_items: "{{ usernames }}"
when: "{{ public_key.value and key_strategy != 'set_jms' }}"
- name: Change public key
lineinfile:
user: "{{ item }}"
dest: /home/{{ item }}/.ssh/authorized_keys regexp='.*{{ public_key.value }}$
state: absent
with_items: "{{ usernames }}"
when: "{{ public_key.value and key_strategy == 'set_jms' }}"
- name: Verify user
ping:
vars:
ansible_user: "{{ item }}"
ansible_pass: "{{ password.value }}"
ansible_ssh_private_key_file: "{{ private_key_file }}"
ansible_connection: "{{ connection_type | default('ssh') }}"
with_items: "{{ usernames }}"

View File

@ -1,5 +0,0 @@
- hosts: all
vars:
connection_type: ssh
roles:
- linux

View File

@ -1,8 +0,0 @@
- name: Verify user
ping:
vars:
ansible_user: "{{ item.username }}"
ansible_pass: "{{ item.username }}"
ansible_connection: "{{ connection_type | default('ssh') }}"
ansible_ssh_private_key_file: "{{ item.private_key_file }}"
with_items: "{{ account_info }}"

View File

@ -39,7 +39,7 @@ class PlatformAutomationSerializer(serializers.ModelSerializer):
'ping_enabled', 'ping_method',
'gather_facts_enabled', 'gather_facts_method',
'create_account_enabled', 'create_account_method',
'change_password_enabled', 'change_password_method',
'change_secret_enabled', 'change_secret_method',
'verify_account_enabled', 'verify_account_method',
'gather_accounts_enabled', 'gather_accounts_method',
]
@ -52,8 +52,8 @@ class PlatformAutomationSerializer(serializers.ModelSerializer):
'verify_account_method': {'label': '校验账号方式'},
'create_account_enabled': {'label': '启用创建账号'},
'create_account_method': {'label': '创建账号方式'},
'change_password_enabled': {'label': '启用账号创建改密'},
'change_password_method': {'label': '账号创建改密方式'},
'change_secret_enabled': {'label': '启用账号创建改密'},
'change_secret_method': {'label': '账号创建改密方式'},
'gather_accounts_enabled': {'label': '启用账号收集'},
'gather_accounts_method': {'label': '收集账号方式'},
}

View File

@ -1 +0,0 @@
from .endpoint import *

Some files were not shown because too many files have changed in this diff Show More