jumpserver/apps/ops/ansible/inventory.py

390 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# ~*~ coding: utf-8 ~*~
import json
import os
import re
from collections import defaultdict
import sys
from django.utils.translation import gettext as _
__all__ = ['JMSInventory']
class JMSInventory:
def __init__(
self, assets, account_policy='privileged_first',
account_prefer='root,Administrator', host_callback=None,
exclude_localhost=False, task_type=None, protocol=None
):
"""
:param assets:
:param account_prefer: account username name if not set use account_policy
:param account_policy: privileged_only, privileged_first, skip
"""
self.assets = self.clean_assets(assets)
self.account_prefer = self.get_account_prefer(account_prefer)
self.account_policy = account_policy
self.host_callback = host_callback
self.exclude_hosts = {}
self.exclude_localhost = exclude_localhost
self.task_type = task_type
self.protocol = protocol
@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) \
.prefetch_related('platform', 'domain', 'accounts')
return assets
@staticmethod
def group_by_platform(assets):
groups = defaultdict(list)
for asset in assets:
groups[asset.platform].append(asset)
return groups
@staticmethod
def get_gateway_ssh_settings(gateway):
platform = gateway.platform
try:
protocol = platform.protocols.get(name='ssh')
except platform.protocols.model.DoesNotExist:
return {}
return protocol.setting
def make_proxy_command(self, gateway, path_dir):
proxy_command_list = [
"ssh", "-o", "Port={}".format(gateway.port),
"-o", "StrictHostKeyChecking=no",
f"{gateway.username}@{gateway.address}"
]
setting = self.get_gateway_ssh_settings(gateway)
if setting.get('nc', False):
proxy_command_list.extend(["nc", "-w", "10", "%h", "%p"])
else:
proxy_command_list.extend(["-W", "%h:%p", "-q"])
if gateway.password:
proxy_command_list.insert(0, f"sshpass -p {gateway.password}")
if gateway.private_key:
proxy_command_list.append(f"-i {gateway.get_private_key_path(path_dir)}")
proxy_command = f"-o ProxyCommand='{' '.join(proxy_command_list)}'"
return {"ansible_ssh_common_args": proxy_command}
@staticmethod
def make_account_ansible_vars(account, path_dir):
var = {
'ansible_user': account.full_username,
}
if not account.secret:
return var
if account.secret_type == 'password':
var['ansible_password'] = account.escape_jinja2_syntax(account.secret)
elif account.secret_type == 'ssh_key':
var['ansible_ssh_private_key_file'] = account.get_private_key_path(path_dir)
return var
@staticmethod
def make_custom_become_ansible_vars(account, su_from_auth, path_dir):
su_method = su_from_auth['ansible_become_method']
var = {
'jms_custom_become': True,
'jms_custom_become_method': su_method,
'jms_custom_become_user': account.su_from.username,
'jms_custom_become_password': account.escape_jinja2_syntax(account.su_from.secret),
'jms_custom_become_private_key_path': account.su_from.get_private_key_path(path_dir)
}
return var
@staticmethod
def make_protocol_setting_vars(host, protocols):
# 针对 ssh 协议的特殊处理
for p in protocols:
if p.name == 'ssh':
if hasattr(p, 'setting'):
setting = getattr(p, 'setting')
host['old_ssh_version'] = setting.get('old_ssh_version', False)
def make_account_vars(self, host, asset, account, automation, protocol, platform, gateway, path_dir,
ansible_config):
from accounts.const import AutomationTypes
if not account:
host['error'] = _("No account available")
return host
port = protocol.port if protocol else 22
host['ansible_host'] = asset.address
host['ansible_port'] = port
su_from = account.su_from
if platform.su_enabled and su_from:
su_from_auth = account.get_ansible_become_auth()
host.update(su_from_auth)
host.update(self.make_custom_become_ansible_vars(account, su_from_auth, path_dir))
elif platform.su_enabled and not su_from and \
self.task_type in (AutomationTypes.change_secret, AutomationTypes.push_account):
host.update(self.make_account_ansible_vars(account, path_dir))
if ansible_config.get('ansible_shell_type') != 'powershell':
host['ansible_become'] = True
host['ansible_become_user'] = 'root'
host['ansible_become_password'] = account.escape_jinja2_syntax(account.secret)
else:
host.update(self.make_account_ansible_vars(account, path_dir))
if platform.is_huawei():
host['ansible_connection'] = 'network_cli'
host['ansible_network_os'] = 'ce'
if gateway:
ansible_connection = host.get('ansible_connection', 'ssh')
if ansible_connection in ('local', 'winrm', 'rdp'):
host['jms_gateway'] = {
'address': gateway.address, 'port': gateway.port,
'username': gateway.username, 'secret': gateway.password,
'private_key_path': gateway.get_private_key_path(path_dir)
}
host['jms_asset']['port'] = port
else:
ansible_ssh_common_args = self.make_proxy_command(gateway, path_dir)
host['jms_asset'].update(ansible_ssh_common_args)
host.update(ansible_ssh_common_args)
def get_primary_protocol(self, ansible_config, protocols):
invalid_protocol = type('protocol', (), {'name': 'null', 'port': 0})
ansible_connection = ansible_config.get('ansible_connection')
# 数值越小,优先级越高,若用户在 ansible_config 中配置了,则提高用户配置方式的优先级
protocol_priority = {'ssh': 10, 'winrm': 9, ansible_connection: 1}
if self.protocol:
protocol_priority.update({self.protocol: 0})
protocol_sorted = sorted(protocols, key=lambda x: protocol_priority.get(x.name, 999))
protocol = protocol_sorted[0] if protocol_sorted else invalid_protocol
return protocol
@staticmethod
def fill_ansible_config(ansible_config, protocol):
if protocol.name in ('ssh', 'winrm', 'rdp'):
ansible_config['ansible_connection'] = protocol.name
if protocol.name == 'winrm':
if protocol.setting.get('use_ssl', False):
ansible_config['ansible_winrm_scheme'] = 'https'
ansible_config['ansible_winrm_transport'] = 'ssl'
ansible_config['ansible_winrm_server_cert_validation'] = 'ignore'
else:
ansible_config['ansible_winrm_scheme'] = 'http'
ansible_config['ansible_winrm_transport'] = 'ntlm'
ansible_config['ansible_winrm_connection_timeout'] = 120
return ansible_config
def asset_to_host(self, asset, account, automation, protocols, platform, path_dir):
try:
ansible_config = dict(automation.ansible_config)
except (AttributeError, TypeError):
ansible_config = {}
protocol = self.get_primary_protocol(ansible_config, protocols)
tp, category = asset.type, asset.category
name = re.sub(r'[ \[\]/]', '_', asset.name)
secret_info = {k: v for k, v in asset.secret_info.items() if v}
host = {
'name': name,
'jms_asset': {
'id': str(asset.id), 'name': asset.name, 'address': asset.address,
'type': tp, 'category': category,
'protocol': protocol.name, 'port': protocol.port,
'spec_info': asset.spec_info, 'secret_info': secret_info,
'protocols': [{'name': p.name, 'port': p.port} for p in protocols],
'origin_address': asset.address
},
'jms_account': {
'id': str(account.id), 'username': account.full_username,
'secret': account.escape_jinja2_syntax(account.secret),
'secret_type': account.secret_type, 'private_key_path': account.get_private_key_path(path_dir)
} if account else None
}
self.make_protocol_setting_vars(host, protocols)
protocols = host['jms_asset']['protocols']
host['jms_asset'].update({f"{p['name']}_port": p['port'] for p in protocols})
if host['jms_account'] and tp == 'oracle':
host['jms_account']['mode'] = 'sysdba' if account.privileged else None
ansible_config = self.fill_ansible_config(ansible_config, protocol)
host.update(ansible_config)
gateway = None
if not asset.is_gateway and asset.domain:
gateway = asset.domain.select_gateway()
self.make_account_vars(
host, asset, account, automation, protocol, platform, gateway, path_dir, ansible_config
)
return host
@staticmethod
def sorted_accounts(accounts):
connectivity_score = {'ok': 2, '-': 1, 'err': 0}
sort_key = lambda x: (x.privileged, connectivity_score.get(x.connectivity, 0), x.date_updated)
accounts_sorted = sorted(accounts, key=sort_key, reverse=True)
return accounts_sorted
def get_asset_sorted_accounts(self, asset):
accounts = list(asset.accounts.filter(is_active=True))
accounts_sorted = self.sorted_accounts(accounts)
return accounts_sorted
@staticmethod
def get_account_prefer(account_prefer):
account_usernames = []
if isinstance(account_prefer, str) and account_prefer:
account_usernames = list(map(lambda x: x.lower(), account_prefer.split(',')))
return account_usernames
def get_refer_account(self, accounts):
account = None
if accounts:
account = list(filter(
lambda a: a.username.lower() in self.account_prefer, accounts
))
account = account[0] if account else None
return account
def select_account(self, asset):
accounts = self.get_asset_sorted_accounts(asset)
if not accounts:
return None
refer_account = self.get_refer_account(accounts)
if refer_account:
return refer_account
account_selected = accounts[0]
if self.account_policy == 'skip':
return None
elif self.account_policy == 'privileged_first':
return account_selected
elif self.account_policy == 'privileged_only' and account_selected.privileged:
return account_selected
else:
return None
@staticmethod
def set_platform_protocol_setting_to_asset(asset, platform_protocols):
asset_protocols = asset.protocols.all()
for p in asset_protocols:
setattr(p, 'setting', platform_protocols.get(p.name, {}))
return asset_protocols
def get_classified_hosts(self, path_dir):
"""
返回三种类型的主机:可运行的、错误的和跳过的
:param path_dir: 存储密钥的路径
:return: dict包含三类主机信息
"""
hosts = []
platform_assets = self.group_by_platform(self.assets)
runnable_hosts = []
error_hosts = []
for platform, assets in platform_assets.items():
automation = platform.automation
platform_protocols = {
p['name']: p['setting'] for p in platform.protocols.values('name', 'setting')
}
for asset in assets:
protocols = self.set_platform_protocol_setting_to_asset(asset, platform_protocols)
account = self.select_account(asset)
host = self.asset_to_host(asset, account, automation, protocols, platform, path_dir)
if not automation.ansible_enabled:
host['error'] = _('Ansible disabled')
if isinstance(host, list):
hosts.extend(host)
else:
hosts.append(host)
# 分类主机
for host in hosts:
if host.get('error'):
self.exclude_hosts[host['name']] = host['error']
error_hosts.append({
'name': host['name'],
'id': host.get('jms_asset', {}).get('id'),
'error': host['error']
})
else:
runnable_hosts.append({
'name': host['name'],
'ip': host['ansible_host'],
'id': host.get('jms_asset', {}).get('id')
})
result = {
'runnable': runnable_hosts,
'error': error_hosts,
}
return result
def generate(self, path_dir):
hosts = []
platform_assets = self.group_by_platform(self.assets)
for platform, assets in platform_assets.items():
automation = platform.automation
platform_protocols = {
p['name']: p['setting'] for p in platform.protocols.values('name', 'setting')
}
for asset in assets:
protocols = self.set_platform_protocol_setting_to_asset(asset, platform_protocols)
account = self.select_account(asset)
host = self.asset_to_host(asset, account, automation, protocols, platform, path_dir)
if not automation.ansible_enabled:
host['error'] = _('Ansible disabled')
if self.host_callback is not None:
host = self.host_callback(
host, asset=asset, account=account,
platform=platform, automation=automation,
path_dir=path_dir
)
if isinstance(host, list):
hosts.extend(host)
else:
hosts.append(host)
exclude_hosts = list(filter(lambda x: x.get('error'), hosts))
if exclude_hosts:
print(_("Skip hosts below:"))
for i, host in enumerate(exclude_hosts, start=1):
print("{}: [{}] \t{}".format(i, host['name'], host['error']))
self.exclude_hosts[host['name']] = host['error']
hosts = list(filter(lambda x: not x.get('error'), hosts))
data = {'all': {'hosts': {}}}
for host in hosts:
name = host.pop('name')
data['all']['hosts'][name] = host
if not self.exclude_localhost:
data['all']['hosts'].update({
'localhost': {
'ansible_host': '127.0.0.1',
'ansible_connection': 'local'
}
})
return data
def write_to_file(self, path):
path_dir = os.path.dirname(path)
if not os.path.exists(path_dir):
os.makedirs(path_dir, 0o700, True)
data = self.generate(path_dir)
with open(path, 'w') as f:
f.write(json.dumps(data, indent=4))