diff --git a/apps/accounts/api/account/task.py b/apps/accounts/api/account/task.py
index 2f3f11dae..697824806 100644
--- a/apps/accounts/api/account/task.py
+++ b/apps/accounts/api/account/task.py
@@ -24,10 +24,11 @@ class AccountsTaskCreateAPI(CreateAPIView):
def perform_create(self, serializer):
data = serializer.validated_data
accounts = data.get('accounts', [])
+ params = data.get('params')
account_ids = [str(a.id) for a in accounts]
if data['action'] == 'push':
- task = push_accounts_to_assets_task.delay(account_ids)
+ task = push_accounts_to_assets_task.delay(account_ids, params)
else:
account = accounts[0]
asset = account.asset
diff --git a/apps/accounts/automations/change_secret/custom/ssh/main.yml b/apps/accounts/automations/change_secret/custom/ssh/main.yml
new file mode 100644
index 000000000..027133496
--- /dev/null
+++ b/apps/accounts/automations/change_secret/custom/ssh/main.yml
@@ -0,0 +1,40 @@
+- hosts: custom
+ gather_facts: no
+ vars:
+ ansible_connection: local
+
+ tasks:
+ - name: Test privileged account
+ ssh_ping:
+ login_host: "{{ jms_asset.address }}"
+ login_port: "{{ jms_asset.port }}"
+ login_user: "{{ jms_account.username }}"
+ login_password: "{{ jms_account.secret }}"
+ login_secret_type: "{{ jms_account.secret_type }}"
+ login_private_key_path: "{{ jms_account.private_key_path }}"
+ register: ping_info
+
+ - name: Change asset password
+ custom_command:
+ login_user: "{{ jms_account.username }}"
+ login_password: "{{ jms_account.secret }}"
+ login_host: "{{ jms_asset.address }}"
+ login_port: "{{ jms_asset.port }}"
+ login_secret_type: "{{ jms_account.secret_type }}"
+ login_private_key_path: "{{ jms_account.private_key_path }}"
+ name: "{{ account.username }}"
+ password: "{{ account.secret }}"
+ commands: "{{ params.commands }}"
+ first_conn_delay_time: "{{ first_conn_delay_time | default(0.5) }}"
+ when: ping_info is succeeded
+ register: change_info
+
+ - name: Verify password
+ ssh_ping:
+ login_user: "{{ account.username }}"
+ login_password: "{{ account.secret }}"
+ login_host: "{{ jms_asset.address }}"
+ login_port: "{{ jms_asset.port }}"
+ when:
+ - ping_info is succeeded
+ - change_info is succeeded
diff --git a/apps/accounts/automations/change_secret/custom/ssh/manifest.yml b/apps/accounts/automations/change_secret/custom/ssh/manifest.yml
new file mode 100644
index 000000000..2796c32c4
--- /dev/null
+++ b/apps/accounts/automations/change_secret/custom/ssh/manifest.yml
@@ -0,0 +1,14 @@
+id: change_secret_by_ssh
+name: Change secret by SSH
+category:
+ - device
+ - host
+type:
+ - all
+method: change_secret
+params:
+ - name: commands
+ type: list
+ label: '自定义命令'
+ default: ['']
+ help_text: '自定义命令中如需包含账号的 username 和 password 字段,请使用 {username}、{password}格式,执行任务时会进行替换 。
比如针对 Linux 主机进行改密,一般需要配置三条命令:
1.passwd {username}
2.{password}
3.{password}'
diff --git a/apps/accounts/automations/push_account/manager.py b/apps/accounts/automations/push_account/manager.py
index fe117f018..9944f12ed 100644
--- a/apps/accounts/automations/push_account/manager.py
+++ b/apps/accounts/automations/push_account/manager.py
@@ -1,6 +1,6 @@
from copy import deepcopy
-from accounts.const import AutomationTypes, SecretType
+from accounts.const import AutomationTypes, SecretType, Connectivity
from assets.const import HostTypes
from common.utils import get_logger
from ..base.manager import AccountBasePlaybookManager
@@ -74,6 +74,7 @@ class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
return
account.secret = new_secret
account.save(update_fields=['secret'])
+ account.set_connectivity(Connectivity.OK)
def on_host_error(self, host, error, result):
pass
diff --git a/apps/accounts/automations/verify_account/custom/main.yml b/apps/accounts/automations/verify_account/custom/main.yml
new file mode 100644
index 000000000..6ad8cd98b
--- /dev/null
+++ b/apps/accounts/automations/verify_account/custom/main.yml
@@ -0,0 +1,14 @@
+- hosts: custom
+ gather_facts: no
+ vars:
+ ansible_connection: local
+
+ tasks:
+ - name: Verify account
+ ssh_ping:
+ login_host: "{{ jms_asset.address }}"
+ login_port: "{{ jms_asset.port }}"
+ login_user: "{{ account.username }}"
+ login_password: "{{ account.secret }}"
+ login_secret_type: "{{ jms_account.secret_type }}"
+ login_private_key_path: "{{ jms_account.private_key_path }}"
diff --git a/apps/accounts/automations/verify_account/custom/manifest.yml b/apps/accounts/automations/verify_account/custom/manifest.yml
new file mode 100644
index 000000000..51c5fedb1
--- /dev/null
+++ b/apps/accounts/automations/verify_account/custom/manifest.yml
@@ -0,0 +1,8 @@
+id: verify_account_by_ssh
+name: Verify account by SSH
+category:
+ - device
+ - host
+type:
+ - all
+method: verify_account
diff --git a/apps/accounts/serializers/account/account.py b/apps/accounts/serializers/account/account.py
index b4c5076cc..626363421 100644
--- a/apps/accounts/serializers/account/account.py
+++ b/apps/accounts/serializers/account/account.py
@@ -97,7 +97,7 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
@staticmethod
def push_account_if_need(instance, push_now, params, stat):
- if not push_now or stat != 'created':
+ if not push_now or stat not in ['created', 'updated']:
return
push_accounts_to_assets_task.delay([str(instance.id)], params)
@@ -407,3 +407,7 @@ class AccountTaskSerializer(serializers.Serializer):
queryset=Account.objects, required=False, allow_empty=True, many=True
)
task = serializers.CharField(read_only=True)
+ params = serializers.JSONField(
+ decoder=None, encoder=None, required=False,
+ style={'base_template': 'textarea.html'}
+ )
diff --git a/apps/accounts/serializers/automations/change_secret.py b/apps/accounts/serializers/automations/change_secret.py
index eba6cd88d..94a7dc428 100644
--- a/apps/accounts/serializers/automations/change_secret.py
+++ b/apps/accounts/serializers/automations/change_secret.py
@@ -58,6 +58,7 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
"Currently only mail sending is supported"
)},
}}
+
@property
def model_type(self):
return AutomationTypes.change_secret
diff --git a/apps/assets/automations/base/manager.py b/apps/assets/automations/base/manager.py
index 9207bb33e..ae9740347 100644
--- a/apps/assets/automations/base/manager.py
+++ b/apps/assets/automations/base/manager.py
@@ -54,7 +54,9 @@ class BasePlaybookManager:
if serializer is None:
return {}
- data = self.params.get(method_id, {})
+ data = self.params.get(method_id)
+ if not data:
+ data = automation_params.get(method_id, {})
params = serializer(data).data
return {
field_name: automation_params.get(field_name, '')
diff --git a/apps/assets/automations/ping/custom/main.yml b/apps/assets/automations/ping/custom/main.yml
new file mode 100644
index 000000000..50911847b
--- /dev/null
+++ b/apps/assets/automations/ping/custom/main.yml
@@ -0,0 +1,14 @@
+- hosts: custom
+ gather_facts: no
+ vars:
+ ansible_connection: local
+
+ tasks:
+ - name: Test asset connection
+ ssh_ping:
+ login_user: "{{ jms_account.username }}"
+ login_password: "{{ jms_account.secret }}"
+ login_host: "{{ jms_asset.address }}"
+ login_port: "{{ jms_asset.port }}"
+ login_secret_type: "{{ jms_account.secret_type }}"
+ login_private_key_path: "{{ jms_account.private_key_path }}"
diff --git a/apps/assets/automations/ping/custom/manifest.yml b/apps/assets/automations/ping/custom/manifest.yml
new file mode 100644
index 000000000..a67cca17d
--- /dev/null
+++ b/apps/assets/automations/ping/custom/manifest.yml
@@ -0,0 +1,8 @@
+id: ping_by_ssh
+name: Ping by SSH
+category:
+ - device
+ - host
+type:
+ - all
+method: ping
diff --git a/apps/assets/automations/ping/manager.py b/apps/assets/automations/ping/manager.py
index 0f166d0ad..d5d0d8d64 100644
--- a/apps/assets/automations/ping/manager.py
+++ b/apps/assets/automations/ping/manager.py
@@ -14,8 +14,10 @@ class PingManager(BasePlaybookManager):
def method_type(cls):
return AutomationTypes.ping
- def host_callback(self, host, asset=None, account=None, **kwargs):
- super().host_callback(host, asset=asset, account=account, **kwargs)
+ def host_callback(self, host, asset=None, account=None, automation=None, **kwargs):
+ super().host_callback(
+ host, asset=asset, account=account, automation=automation, **kwargs
+ )
self.host_asset_and_account_mapper[host['name']] = (asset, account)
return host
diff --git a/apps/assets/const/base.py b/apps/assets/const/base.py
index c90ab7320..99ff06314 100644
--- a/apps/assets/const/base.py
+++ b/apps/assets/const/base.py
@@ -6,12 +6,27 @@ from .protocol import Protocol
class Type:
def __init__(self, label, value):
+ self.name = value
self.label = label
self.value = value
def __str__(self):
return self.value
+ def __add__(self, other):
+ if isinstance(other, str):
+ return str(str(self) + other)
+ raise TypeError("unsupported operand type(s) for +: '{}' and '{}'".format(
+ type(self), type(other))
+ )
+
+ def __radd__(self, other):
+ if isinstance(other, str):
+ return str(other + str(self))
+ raise TypeError("unsupported operand type(s) for +(r): '{}' and '{}'".format(
+ type(self), type(other))
+ )
+
class BaseType(TextChoices):
"""
@@ -77,10 +92,7 @@ class BaseType(TextChoices):
@classmethod
def get_types(cls):
- tps = cls._get_choices_to_types()
- if not has_valid_xpack_license():
- tps = cls.get_community_types()
- return tps
+ return cls._get_choices_to_types()
@classmethod
def get_community_types(cls):
@@ -88,4 +100,9 @@ class BaseType(TextChoices):
@classmethod
def get_choices(cls):
+ if not has_valid_xpack_license():
+ return [
+ (tp.value, tp.label)
+ for tp in cls.get_community_types()
+ ]
return cls.choices
diff --git a/apps/assets/const/device.py b/apps/assets/const/device.py
index 5e8f8f879..9dfd07ca0 100644
--- a/apps/assets/const/device.py
+++ b/apps/assets/const/device.py
@@ -32,15 +32,16 @@ class DeviceTypes(BaseType):
def _get_automation_constrains(cls) -> dict:
return {
'*': {
- 'ansible_enabled': False,
+ 'ansible_enabled': True,
'ansible_config': {
'ansible_connection': 'local',
+ 'first_conn_delay_time': 0.5,
},
- 'ping_enabled': False,
+ 'ping_enabled': True,
'gather_facts_enabled': False,
'gather_accounts_enabled': False,
- 'verify_account_enabled': False,
- 'change_secret_enabled': False,
+ 'verify_account_enabled': True,
+ 'change_secret_enabled': True,
'push_account_enabled': False
}
}
diff --git a/apps/assets/models/asset/common.py b/apps/assets/models/asset/common.py
index 9c81dd0e9..b39119b81 100644
--- a/apps/assets/models/asset/common.py
+++ b/apps/assets/models/asset/common.py
@@ -90,6 +90,7 @@ class Protocol(models.Model):
name = models.CharField(max_length=32, verbose_name=_("Name"))
port = models.IntegerField(verbose_name=_("Port"))
asset = models.ForeignKey('Asset', on_delete=models.CASCADE, related_name='protocols', verbose_name=_("Asset"))
+ _setting = None
def __str__(self):
return '{}/{}'.format(self.name, self.port)
@@ -102,8 +103,14 @@ class Protocol(models.Model):
@property
def setting(self):
+ if self._setting is not None:
+ return self._setting
return self.asset_platform_protocol.get('setting', {})
+ @setting.setter
+ def setting(self, value):
+ self._setting = value
+
@property
def public(self):
return self.asset_platform_protocol.get('public', True)
diff --git a/apps/assets/serializers/platform.py b/apps/assets/serializers/platform.py
index c542cc20a..b010a6ed0 100644
--- a/apps/assets/serializers/platform.py
+++ b/apps/assets/serializers/platform.py
@@ -168,7 +168,7 @@ class PlatformSerializer(WritableNestedModelSerializer):
su_enabled = attrs.get('su_enabled', False) and self.constraints.get('su_enabled', False)
automation = attrs.get('automation', {})
automation['ansible_enabled'] = automation.get('ansible_enabled', False) \
- and self.constraints.get('ansible_enabled', False)
+ and self.constraints['automation'].get('ansible_enabled', False)
attrs.update({
'domain_enabled': domain_enabled,
'su_enabled': su_enabled,
diff --git a/apps/assets/utils/k8s.py b/apps/assets/utils/k8s.py
index 0ed17835f..8e12e8de8 100644
--- a/apps/assets/utils/k8s.py
+++ b/apps/assets/utils/k8s.py
@@ -4,6 +4,7 @@ from urllib.parse import urlencode
from kubernetes import client
from kubernetes.client import api_client
from kubernetes.client.api import core_v1_api
+from kubernetes.client.exceptions import ApiException
from common.utils import get_logger
from ..const import CloudTypes, Category
@@ -65,9 +66,13 @@ class KubernetesClient:
proxy_url = cls.get_proxy_url(asset)
k8s = cls(k8s_url, secret, proxy=proxy_url)
func_name = f'get_{tp}s'
+ data = []
if hasattr(k8s, func_name):
- return getattr(k8s, func_name)(*args)
- return []
+ try:
+ data = getattr(k8s, func_name)(*args)
+ except ApiException as e:
+ logger.error(e.reason)
+ return data
class KubernetesTree:
diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py
index 2695851bb..fc124b210 100644
--- a/apps/ops/ansible/inventory.py
+++ b/apps/ops/ansible/inventory.py
@@ -150,7 +150,8 @@ class JMSInventory:
},
'jms_account': {
'id': str(account.id), 'username': account.username,
- 'secret': account.secret, 'secret_type': account.secret_type
+ 'secret': account.secret, 'secret_type': account.secret_type,
+ 'private_key_path': account.private_key_path
} if account else None
}
diff --git a/apps/ops/ansible/modules/custom_command.py b/apps/ops/ansible/modules/custom_command.py
new file mode 100644
index 000000000..4205e7088
--- /dev/null
+++ b/apps/ops/ansible/modules/custom_command.py
@@ -0,0 +1,115 @@
+#!/usr/bin/python
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: custom_command
+short_description: Adds or removes a user with custom commands by ssh
+description:
+ - You can add or edit users using ssh with custom commands.
+
+options:
+ protocol:
+ default: ssh
+ choices: [ssh]
+ description:
+ - C(ssh) The remote asset is connected using ssh.
+ type: str
+ name:
+ description:
+ - The name of the user to add or remove.
+ required: true
+ aliases: [user]
+ type: str
+ password:
+ description:
+ - The password to use for the user.
+ type: str
+ aliases: [pass]
+ commands:
+ description:
+ - Custom change password commands.
+ type: list
+ required: true
+ first_conn_delay_time:
+ description:
+ - Delay for executing the command after SSH connection(unit: s)
+ type: float
+ required: false
+'''
+
+EXAMPLES = '''
+- name: Create user with name 'jms' and password '123456'.
+ custom_command:
+ login_host: "localhost"
+ login_port: 22
+ login_user: "admin"
+ login_password: "123456"
+ name: "jms"
+ password: "123456"
+ commands: ['passwd {username}', '{password}', '{password}']
+'''
+
+RETURN = '''
+name:
+ description: The name of the user to add.
+ returned: success
+ type: str
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+from ops.ansible.modules_utils.custom_common import (
+ SSHClient, ssh_common_argument_spec
+)
+
+
+def get_commands(module):
+ username = module.params['name']
+ password = module.params['password']
+ commands = module.params['commands'] or []
+ for index, command in enumerate(commands):
+ commands[index] = command.format(
+ username=username, password=password
+ )
+ return commands
+
+# =========================================
+# Module execution.
+#
+
+
+def main():
+ argument_spec = ssh_common_argument_spec()
+ argument_spec.update(
+ name=dict(required=True, aliases=['user']),
+ password=dict(aliases=['pass'], no_log=True),
+ commands=dict(type='list', required=False),
+ first_conn_delay_time=dict(
+ type='float', required=False, default=0.5
+ ),
+ )
+ module = AnsibleModule(argument_spec=argument_spec)
+
+ ssh_client = SSHClient(module)
+ commands = get_commands(module)
+ if not commands:
+ module.fail_json(
+ msg='No command found, please go to the platform details to add'
+ )
+ err = ssh_client.execute(commands)
+ if err:
+ module.fail_json(
+ msg='There was a problem executing the command: %s' % err
+ )
+
+ user = module.params['name']
+ module.exit_json(changed=True, user=user)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/apps/ops/ansible/modules/oracle_user.py b/apps/ops/ansible/modules/oracle_user.py
index 9e23fe70c..c1d485e40 100644
--- a/apps/ops/ansible/modules/oracle_user.py
+++ b/apps/ops/ansible/modules/oracle_user.py
@@ -69,7 +69,7 @@ EXAMPLES = '''
oracle_user:
hostname: "remote server"
login_database: "helowin"
- login_username: "system"
+ login_user: "system"
login_password: "123456"
name: "jms"
password: "123456"
@@ -78,7 +78,7 @@ EXAMPLES = '''
oracle_user:
hostname: "remote server"
login_database: "helowin"
- login_username: "system"
+ login_user: "system"
login_password: "123456"
name: "jms"
state: "absent"
diff --git a/apps/ops/ansible/modules/ssh_ping.py b/apps/ops/ansible/modules/ssh_ping.py
new file mode 100644
index 000000000..15a30eb0e
--- /dev/null
+++ b/apps/ops/ansible/modules/ssh_ping.py
@@ -0,0 +1,69 @@
+#!/usr/bin/python
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: custom_ssh_ping
+short_description: Use ssh to probe whether an asset is connectable
+description:
+ - Use ssh to probe whether an asset is connectable
+'''
+
+EXAMPLES = '''
+- name: >
+ Ping asset server.
+ custom_ssh_ping:
+ login_host: 127.0.0.1
+ login_port: 22
+ login_user: jms
+ login_password: password
+'''
+
+RETURN = '''
+is_available:
+ description: MongoDB server availability.
+ returned: always
+ type: bool
+ sample: true
+conn_err_msg:
+ description: Connection error message.
+ returned: always
+ type: str
+ sample: ''
+'''
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+from ops.ansible.modules_utils.custom_common import (
+ SSHClient, ssh_common_argument_spec
+)
+
+
+# =========================================
+# Module execution.
+#
+
+
+def main():
+ options = ssh_common_argument_spec()
+ module = AnsibleModule(argument_spec=options, supports_check_mode=True,)
+
+ result = {
+ 'changed': False, 'is_available': True
+ }
+ client = SSHClient(module)
+ err = client.connect()
+ if err:
+ module.fail_json(msg='Unable to connect to asset: %s' % err)
+ result['is_available'] = False
+
+ return module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/apps/ops/ansible/modules_utils/custom_common.py b/apps/ops/ansible/modules_utils/custom_common.py
new file mode 100644
index 000000000..07c2b6648
--- /dev/null
+++ b/apps/ops/ansible/modules_utils/custom_common.py
@@ -0,0 +1,68 @@
+import time
+
+import paramiko
+
+from paramiko.ssh_exception import SSHException, NoValidConnectionsError
+
+
+def ssh_common_argument_spec():
+ options = dict(
+ login_host=dict(type='str', required=False, default='localhost'),
+ login_port=dict(type='int', required=False, default=22),
+ login_user=dict(type='str', required=False, default='root'),
+ login_password=dict(type='str', required=False, no_log=True),
+ login_secret_type=dict(type='str', required=False, default='password'),
+ login_private_key_path=dict(type='str', required=False, no_log=True),
+ )
+ return options
+
+
+class SSHClient:
+ def __init__(self, module):
+ self.module = module
+ self.is_connect = False
+ self.client = paramiko.SSHClient()
+ self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+
+ def get_connect_params(self):
+ params = {
+ 'allow_agent': False, 'look_for_keys': False,
+ 'hostname': self.module.params['login_host'],
+ 'port': self.module.params['login_port'],
+ 'username': self.module.params['login_user'],
+ }
+ secret_type = self.module.params['login_secret_type']
+ if secret_type == 'ssh_key':
+ params['key_filename'] = self.module.params['login_private_key_path']
+ else:
+ params['password'] = self.module.params['login_password']
+ return params
+
+ def connect(self):
+ try:
+ self.client.connect(**self.get_connect_params())
+ except (SSHException, NoValidConnectionsError) as err:
+ err_msg = str(err)
+ else:
+ self.is_connect = True
+ err_msg = ''
+ return err_msg
+
+ def execute(self, commands):
+ if not self.is_connect:
+ self.connect()
+
+ channel = self.client.invoke_shell()
+ # 读取首次登陆终端返回的消息
+ channel.recv(2048)
+ # 网络设备一般登录有延迟,等终端有返回后再执行命令
+ delay_time = self.module.params['first_conn_delay_time']
+ time.sleep(delay_time)
+ err_msg = ''
+ try:
+ for command in commands:
+ channel.send(command + '\n')
+ time.sleep(0.3)
+ except SSHException as e:
+ err_msg = str(e)
+ return err_msg
diff --git a/apps/terminal/api/applet/applet.py b/apps/terminal/api/applet/applet.py
index 116aec94e..ae984ba2f 100644
--- a/apps/terminal/api/applet/applet.py
+++ b/apps/terminal/api/applet/applet.py
@@ -64,7 +64,7 @@ class DownloadUploadMixin:
if instance and not update:
return Response({'error': 'Applet already exists: {}'.format(name)}, status=400)
- applet, serializer = Applet.install_from_dir(tmp_dir)
+ applet, serializer = Applet.install_from_dir(tmp_dir, builtin=False)
return Response(serializer.data, status=201)
@action(detail=True, methods=['get'])