diff --git a/apps/assets/automations/base/manager.py b/apps/assets/automations/base/manager.py index 7184d91f7..63b490fd9 100644 --- a/apps/assets/automations/base/manager.py +++ b/apps/assets/automations/base/manager.py @@ -1,11 +1,12 @@ import json import os import shutil +import yaml + from collections import defaultdict from hashlib import md5 from socket import gethostname -import yaml from django.conf import settings from django.utils import timezone from django.utils.translation import gettext as _ @@ -239,10 +240,12 @@ class BasePlaybookManager: jms_asset, jms_gateway = host['jms_asset'], host.get('gateway') if not jms_gateway: continue + server = SSHTunnelForwarder( (jms_gateway['address'], jms_gateway['port']), ssh_username=jms_gateway['username'], ssh_password=jms_gateway['secret'], + ssh_pkey=jms_gateway['private_key_path'], remote_bind_address=(jms_asset['address'], jms_asset['port']) ) try: @@ -252,8 +255,8 @@ class BasePlaybookManager: print('\033[31m %s \033[0m\n' % err_msg) not_valid.append(k) else: - jms_asset['address'] = '127.0.0.1' - jms_asset['port'] = server.local_bind_port + host['ansible_host'] = jms_asset['address'] = '127.0.0.1' + host['ansible_port'] = jms_asset['port'] = server.local_bind_port servers.append(server) # 网域不可连接的,就不继续执行此资源的后续任务了 diff --git a/apps/assets/const/host.py b/apps/assets/const/host.py index 7b6c8818c..afb92a447 100644 --- a/apps/assets/const/host.py +++ b/apps/assets/const/host.py @@ -36,7 +36,7 @@ class HostTypes(BaseType): 'choices': ['ssh', 'telnet', 'vnc', 'rdp'] }, cls.WINDOWS: { - 'choices': ['rdp', 'ssh', 'vnc'] + 'choices': ['rdp', 'ssh', 'vnc', 'winrm'] } } @@ -58,7 +58,7 @@ class HostTypes(BaseType): cls.WINDOWS: { 'ansible_config': { 'ansible_shell_type': 'cmd', - 'ansible_connection': 'ssh', + 'ansible_connection': 'smart', }, }, cls.OTHER_HOST: { diff --git a/apps/assets/const/protocol.py b/apps/assets/const/protocol.py index 39d3f3112..eef3269d3 100644 --- a/apps/assets/const/protocol.py +++ b/apps/assets/const/protocol.py @@ -10,6 +10,7 @@ class Protocol(ChoicesMixin, models.TextChoices): rdp = 'rdp', 'RDP' telnet = 'telnet', 'Telnet' vnc = 'vnc', 'VNC' + winrm = 'winrm', 'WinRM' mysql = 'mysql', 'MySQL' mariadb = 'mariadb', 'MariaDB' @@ -51,6 +52,13 @@ class Protocol(ChoicesMixin, models.TextChoices): 'port': 23, 'secret_types': ['password'], }, + cls.winrm: { + 'port': 5985, + 'secret_types': ['password'], + 'setting': { + 'use_ssl': False, + } + }, } @classmethod diff --git a/apps/assets/migrations/0111_auto_20230321_1633.py b/apps/assets/migrations/0111_auto_20230321_1633.py index 314d2ed7f..fce00cc98 100644 --- a/apps/assets/migrations/0111_auto_20230321_1633.py +++ b/apps/assets/migrations/0111_auto_20230321_1633.py @@ -2,6 +2,8 @@ from django.db import migrations, models +from assets.const import AllTypes + def migrate_platform_charset(apps, schema_editor): platform_model = apps.get_model('assets', 'Platform') @@ -20,6 +22,11 @@ def migrate_platform_protocol_primary(apps, schema_editor): p.save() +def migrate_internal_platforms(apps, schema_editor): + platform_cls = apps.get_model('assets', 'Platform') + AllTypes.create_or_update_internal_platforms(platform_cls) + + class Migration(migrations.Migration): dependencies = [ @@ -34,4 +41,5 @@ class Migration(migrations.Migration): ), migrations.RunPython(migrate_platform_charset), migrations.RunPython(migrate_platform_protocol_primary), + migrations.RunPython(migrate_internal_platforms), ] diff --git a/apps/assets/serializers/platform.py b/apps/assets/serializers/platform.py index 20824cba9..300b3dd65 100644 --- a/apps/assets/serializers/platform.py +++ b/apps/assets/serializers/platform.py @@ -41,6 +41,9 @@ class ProtocolSettingSerializer(serializers.Serializer): # Redis auth_username = serializers.BooleanField(default=False, label=_("Auth with username")) + # WinRM + use_ssl = serializers.BooleanField(default=False, label=_("Use SSL")) + class PlatformAutomationSerializer(serializers.ModelSerializer): class Meta: diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py index 6abf6d18e..2695851bb 100644 --- a/apps/ops/ansible/inventory.py +++ b/apps/ops/ansible/inventory.py @@ -72,15 +72,14 @@ class JMSInventory: var['ansible_ssh_private_key_file'] = account.private_key_path return var - def make_ssh_account_vars(self, host, asset, account, automation, protocols, platform, gateway): + def make_account_vars(self, host, asset, account, automation, protocol, platform, gateway): if not account: host['error'] = _("No account available") return host - ssh_protocol_matched = list(filter(lambda x: x.name == 'ssh', protocols)) - ssh_protocol = ssh_protocol_matched[0] if ssh_protocol_matched else None + port = protocol.port if protocol else 22 host['ansible_host'] = asset.address - host['ansible_port'] = ssh_protocol.port if ssh_protocol else 22 + host['ansible_port'] = port su_from = account.su_from if platform.su_enabled and su_from: @@ -97,28 +96,55 @@ class JMSInventory: host.update(self.make_account_ansible_vars(account)) if gateway: - host.update(self.make_proxy_command(gateway)) + ansible_connection = host.get('ansible_connection', 'ssh') + if ansible_connection in ('local', 'winrm'): + host['gateway'] = { + 'address': gateway.address, 'port': gateway.port, + 'username': gateway.username, 'secret': gateway.password, + 'private_key_path': gateway.private_key_path + } + host['jms_asset']['port'] = port + else: + host.update(self.make_proxy_command(gateway)) @staticmethod - def get_primary_protocol(protocols): - if protocols: - primary = protocols[0] - protocol = primary.name - port = primary.port - else: - protocol = 'null' - port = 0 - return protocol, port + def get_primary_protocol(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} + 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'): + 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'] = 'plaintext' + return ansible_config def asset_to_host(self, asset, account, automation, protocols, platform): - protocol, port = self.get_primary_protocol(protocols) + try: + ansible_config = dict(automation.ansible_config) + except (AttributeError, TypeError): + ansible_config = {} + + protocol = self.get_primary_protocol(ansible_config, protocols) host = { 'name': '{}'.format(asset.name.replace(' ', '_')), 'jms_asset': { 'id': str(asset.id), 'name': asset.name, 'address': asset.address, 'type': asset.type, 'category': asset.category, - 'protocol': protocol, 'port': port, + 'protocol': protocol.name, 'port': protocol.port, 'spec_info': asset.spec_info, 'secret_info': asset.secret_info, 'protocols': [{'name': p.name, 'port': p.port} for p in protocols], }, @@ -131,25 +157,16 @@ class JMSInventory: if host['jms_account'] and asset.platform.type == 'oracle': host['jms_account']['mode'] = 'sysdba' if account.privileged else None - try: - ansible_config = dict(automation.ansible_config) - except Exception as e: - ansible_config = {} - ansible_connection = ansible_config.get('ansible_connection', 'ssh') + 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() - if ansible_connection == 'local': - if gateway: - host['gateway'] = { - 'address': gateway.address, 'port': gateway.port, - 'username': gateway.username, 'secret': gateway.password - } - else: - self.make_ssh_account_vars(host, asset, account, automation, protocols, platform, gateway) + self.make_account_vars( + host, asset, account, automation, protocol, platform, gateway + ) return host def get_asset_sorted_accounts(self, asset): @@ -194,14 +211,23 @@ class JMSInventory: 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 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 = asset.protocols.all() + 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) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 310bc9de6..6457f2104 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -65,6 +65,7 @@ pyjwkest==1.4.2 jsonfield2==4.0.0.post0 geoip2==4.5.0 ipip-ipdb==1.6.1 +pywinrm==0.4.3 # Django environment Django==3.2.17 django-bootstrap3==14.2.0