diff --git a/apps/assets/api/account/account.py b/apps/assets/api/account/account.py index 8e8e320bd..4aef92de0 100644 --- a/apps/assets/api/account/account.py +++ b/apps/assets/api/account/account.py @@ -43,7 +43,7 @@ class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet): serializer_classes = { 'default': serializers.AccountSecretSerializer } - http_method_names = ['get'] + http_method_names = ['get', 'options'] # Todo: 记得打开 # permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)] rbac_perms = { diff --git a/apps/assets/automations/base/manager.py b/apps/assets/automations/base/manager.py index e2faecd64..a927987d1 100644 --- a/apps/assets/automations/base/manager.py +++ b/apps/assets/automations/base/manager.py @@ -1,19 +1,68 @@ import os -import shutil import yaml +import shutil +from hashlib import md5 +from copy import deepcopy +from socket import gethostname from collections import defaultdict from django.conf import settings from django.utils import timezone +from django.db.models import Model from django.utils.translation import gettext as _ from common.utils import get_logger +from common.utils import ssh_pubkey_gen, ssh_key_string_to_obj +from assets.const import SecretType from assets.automations.methods import platform_automation_methods from ops.ansible import JMSInventory, PlaybookRunner, DefaultCallback logger = get_logger(__name__) +class PushOrVerifyHostCallbackMixin: + execution: callable + host_account_mapper: dict + ignore_account: bool + generate_public_key: callable + generate_private_key_path: callable + + def host_callback(self, host, asset=None, account=None, automation=None, path_dir=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 self.ignore_account and account: + accounts = accounts.exclude(id=account.id) + + if '*' not in self.execution.snapshot['accounts']: + accounts = accounts.filter(username__in=self.execution.snapshot['accounts']) + + inventory_hosts = [] + for account in accounts: + h = deepcopy(host) + h['name'] += '_' + account.username + self.host_account_mapper[h['name']] = account + secret = account.secret + + private_key_path = None + if account.secret_type == SecretType.ssh_key: + private_key_path = self.generate_private_key_path(secret, path_dir) + secret = self.generate_public_key(secret) + + h['secret_type'] = account.secret_type + h['account'] = { + 'name': account.name, + 'username': account.username, + 'secret_type': account.secret_type, + 'secret': secret, + 'private_key_path': private_key_path + } + inventory_hosts.append(h) + return inventory_hosts + + class PlaybookCallback(DefaultCallback): def playbook_on_stats(self, event_data, **kwargs): super().playbook_on_stats(event_data, **kwargs) @@ -66,20 +115,33 @@ class BasePlaybookManager: 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 + 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'] = _('{} disabled'.format(self.__class__.method_type())) return host return host + @staticmethod + def generate_public_key(private_key): + return ssh_pubkey_gen(private_key=private_key, hostname=gethostname()) + + @staticmethod + def generate_private_key_path(secret, path_dir): + key_name = '.' + md5(secret.encode('utf-8')).hexdigest() + key_path = os.path.join(path_dir, key_name) + if not os.path.exists(key_path): + ssh_key_string_to_obj(secret, password=None).write_private_key_file(key_path) + os.chmod(key_path, 0o400) + return key_path + def generate_inventory(self, platformed_assets, inventory_path): inventory = JMSInventory( - manager=self, assets=platformed_assets, account_policy=self.ansible_account_policy, + host_callback=self.host_callback, ) inventory.write_to_file(inventory_path) @@ -105,7 +167,7 @@ class BasePlaybookManager: def get_runners(self): runners = [] for platform, assets in self.get_assets_group_by_platform().items(): - assets_bulked = [assets[i:i+self.bulk_size] for i in range(0, len(assets), self.bulk_size)] + assets_bulked = [assets[i:i + self.bulk_size] for i in range(0, len(assets), self.bulk_size)] for i, _assets in enumerate(assets_bulked, start=1): sub_dir = '{}_{}'.format(platform.name, i) @@ -148,7 +210,7 @@ class BasePlaybookManager: print(" inventory: {}".format(runner.inventory)) print(" playbook: {}".format(runner.playbook)) - def run(self, *args, **kwargs): + def run(self, *args, **kwargs): runners = self.get_runners() if len(runners) > 1: print("### 分批次执行开始任务, 总共 {}\n".format(len(runners))) diff --git a/apps/assets/automations/change_secret/database/postgresql/main.yml b/apps/assets/automations/change_secret/database/postgresql/main.yml index 40c326704..4ef9d65ab 100644 --- a/apps/assets/automations/change_secret/database/postgresql/main.yml +++ b/apps/assets/automations/change_secret/database/postgresql/main.yml @@ -10,7 +10,7 @@ login_password: "{{ jms_account.secret }}" login_host: "{{ jms_asset.address }}" login_port: "{{ jms_asset.port }}" - login_db: "{{ jms_asset.database }}" + login_db: "{{ jms_asset.category_property.db_name }}" register: db_info - name: Display PostgreSQL version @@ -24,7 +24,7 @@ login_password: "{{ jms_account.secret }}" login_host: "{{ jms_asset.address }}" login_port: "{{ jms_asset.port }}" - db: "{{ jms_asset.database }}" + db: "{{ jms_asset.category_property.db_name }}" name: "{{ account.username }}" password: "{{ account.secret }}" when: db_info is succeeded @@ -36,7 +36,7 @@ login_password: "{{ account.secret }}" login_host: "{{ jms_asset.address }}" login_port: "{{ jms_asset.port }}" - db: "{{ jms_asset.database }}" + db: "{{ jms_asset.category_property.db_name }}" when: - db_info is succeeded - change_info is succeeded diff --git a/apps/assets/automations/change_secret/host/aix/main.yml b/apps/assets/automations/change_secret/host/aix/main.yml deleted file mode 100644 index 1a4e6a6a4..000000000 --- a/apps/assets/automations/change_secret/host/aix/main.yml +++ /dev/null @@ -1,58 +0,0 @@ -- hosts: demo - gather_facts: no - tasks: - - name: Test privileged account - ansible.builtin.ping: - # - # - name: print variables - # debug: - # msg: "Username: {{ account.username }}, Secret: {{ account.secret }}, Secret type: {{ secret_type }}" - - - name: Change password - ansible.builtin.user: - name: "{{ account.username }}" - password: "{{ account.secret | password_hash('sha512') }}" - update_password: always - when: secret_type == "password" - - - name: create user If it already exists, no operation will be performed - ansible.builtin.user: - name: "{{ account.username }}" - when: secret_type == "ssh_key" - - - name: remove jumpserver ssh key - ansible.builtin.lineinfile: - dest: "{{ kwargs.dest }}" - regexp: "{{ kwargs.regexp }}" - state: absent - when: - - secret_type == "ssh_key" - - kwargs.strategy == "set_jms" - - - name: Change SSH key - ansible.builtin.authorized_key: - user: "{{ account.username }}" - key: "{{ account.secret }}" - exclusive: "{{ kwargs.exclusive }}" - when: secret_type == "ssh_key" - - - name: Refresh connection - ansible.builtin.meta: reset_connection - - - name: Verify password - ansible.builtin.ping: - become: no - vars: - ansible_user: "{{ account.username }}" - ansible_password: "{{ account.secret }}" - ansible_become: no - when: secret_type == "password" - - - name: Verify SSH key - ansible.builtin.ping: - become: no - vars: - ansible_user: "{{ account.username }}" - ansible_ssh_private_key_file: "{{ account.private_key_path }}" - ansible_become: no - when: secret_type == "ssh_key" diff --git a/apps/assets/automations/change_secret/host/aix/manifest.yml b/apps/assets/automations/change_secret/host/aix/manifest.yml deleted file mode 100644 index 94d93f3af..000000000 --- a/apps/assets/automations/change_secret/host/aix/manifest.yml +++ /dev/null @@ -1,6 +0,0 @@ -id: change_secret_aix -name: Change password for AIX -category: host -type: - - aix -method: change_secret diff --git a/apps/assets/automations/change_secret/manager.py b/apps/assets/automations/change_secret/manager.py index 4ac49676b..b8868acd1 100644 --- a/apps/assets/automations/change_secret/manager.py +++ b/apps/assets/automations/change_secret/manager.py @@ -1,14 +1,11 @@ -import os import random import string -from hashlib import md5 from copy import deepcopy -from socket import gethostname from collections import defaultdict from django.utils import timezone -from common.utils import lazyproperty, gen_key_pair, ssh_pubkey_gen, ssh_key_string_to_obj +from common.utils import lazyproperty, gen_key_pair from assets.models import ChangeSecretRecord from assets.const import ( AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy, DEFAULT_PASSWORD_RULES @@ -39,19 +36,6 @@ class ChangeSecretManager(BasePlaybookManager): private_key, public_key = gen_key_pair() return private_key - @staticmethod - def generate_public_key(private_key): - return ssh_pubkey_gen(private_key=private_key, hostname=gethostname()) - - @staticmethod - def generate_private_key_path(secret, path_dir): - key_name = '.' + md5(secret.encode('utf-8')).hexdigest() - key_path = os.path.join(path_dir, key_name) - if not os.path.exists(key_path): - ssh_key_string_to_obj(secret, password=None).write_private_key_file(key_path) - os.chmod(key_path, 0o400) - return key_path - def generate_password(self): kwargs = self.execution.snapshot['password_rules'] or {} length = int(kwargs.get('length', DEFAULT_PASSWORD_RULES['length'])) diff --git a/apps/assets/automations/endpoint.py b/apps/assets/automations/endpoint.py index 11330370a..c6eb04593 100644 --- a/apps/assets/automations/endpoint.py +++ b/apps/assets/automations/endpoint.py @@ -1,6 +1,8 @@ from .change_secret.manager import ChangeSecretManager from .gather_facts.manager import GatherFactsManager from .gather_accounts.manager import GatherAccountsManager +from .verify_account.manager import VerifyAccountManager +from .push_account.manager import PushAccountManager from ..const import AutomationTypes @@ -9,6 +11,8 @@ class ExecutionManager: AutomationTypes.change_secret: ChangeSecretManager, AutomationTypes.gather_facts: GatherFactsManager, AutomationTypes.gather_accounts: GatherAccountsManager, + AutomationTypes.verify_account: VerifyAccountManager, + AutomationTypes.push_account: PushAccountManager, } def __init__(self, execution): diff --git a/apps/assets/automations/gather_accounts/database/mysql/main.yml b/apps/assets/automations/gather_accounts/database/mysql/main.yml index cc934f20f..4b166322a 100644 --- a/apps/assets/automations/gather_accounts/database/mysql/main.yml +++ b/apps/assets/automations/gather_accounts/database/mysql/main.yml @@ -1,7 +1,7 @@ - hosts: mysql gather_facts: no vars: - ansible_python_interpreter: /usr/local/bin/python + ansible_python_interpreter: /Users/xiaofeng/Desktop/jumpserver/venv/bin/python tasks: - name: Get info @@ -9,7 +9,7 @@ login_user: "{{ jms_account.username }}" login_password: "{{ jms_account.secret }}" login_host: "{{ jms_asset.address }}" - login_port: "{{ jms_asset.port }}" + login_port: 1234 filter: users register: db_info diff --git a/apps/assets/automations/gather_accounts/database/postgresql/main.yml b/apps/assets/automations/gather_accounts/database/postgresql/main.yml index 2e12f51e5..cf0320627 100644 --- a/apps/assets/automations/gather_accounts/database/postgresql/main.yml +++ b/apps/assets/automations/gather_accounts/database/postgresql/main.yml @@ -10,7 +10,7 @@ login_password: "{{ jms_account.secret }}" login_host: "{{ jms_asset.address }}" login_port: "{{ jms_asset.port }}" - login_db: "{{ jms_asset.database }}" + login_db: "{{ jms_asset.category_property.db_name }}" filter: "roles" register: db_info diff --git a/apps/assets/automations/gather_accounts/filter.py b/apps/assets/automations/gather_accounts/filter.py index 0c8f32536..ebaf6d9b1 100644 --- a/apps/assets/automations/gather_accounts/filter.py +++ b/apps/assets/automations/gather_accounts/filter.py @@ -39,8 +39,11 @@ class GatherAccountsFilter: @staticmethod def windows_filter(info): - # TODO + info = info[4:-2] result = {} + for i in info: + for username in i.split(): + result[username] = {} return result def run(self, method_id_meta_mapper, info): diff --git a/apps/assets/automations/gather_accounts/host/posix/main.yml b/apps/assets/automations/gather_accounts/host/posix/main.yml index 97326431d..a64323f9d 100644 --- a/apps/assets/automations/gather_accounts/host/posix/main.yml +++ b/apps/assets/automations/gather_accounts/host/posix/main.yml @@ -2,8 +2,10 @@ gather_facts: no tasks: - name: Gather posix account - ansible.builtin.win_shell: - cmd: net user + ansible.builtin.shell: + cmd: > + users=$(getent passwd | grep -v nologin | grep -v shutdown | awk -F":" '{ print $1 }');for i in $users; + do last -w -F $i -1 | head -1 | grep -v ^$ | awk '{ print $1"@"$3"@"$5,$6,$7,$8 }';done register: result - name: Define info by set_fact diff --git a/apps/assets/automations/gather_accounts/host/windows/main.yml b/apps/assets/automations/gather_accounts/host/windows/main.yml index 377ffd10a..97326431d 100644 --- a/apps/assets/automations/gather_accounts/host/windows/main.yml +++ b/apps/assets/automations/gather_accounts/host/windows/main.yml @@ -1,18 +1,14 @@ -- hosts: windows - gather_facts: yes +- hosts: demo + gather_facts: no tasks: - - name: Get info - set_fact: - info: - arch: "{{ ansible_architecture2 }}" - distribution: "{{ ansible_distribution }}" - distribution_version: "{{ ansible_distribution_version }}" - kernel: "{{ ansible_kernel }}" - vendor: "{{ ansible_system_vendor }}" - model: "{{ ansible_product_name }}" - sn: "{{ ansible_product_serial }}" - cpu_vcpus: "{{ ansible_processor_vcpus }}" - memory: "{{ ansible_memtotal_mb }}" + - name: Gather posix account + ansible.builtin.win_shell: + cmd: net user + register: result + + - name: Define info by set_fact + ansible.builtin.set_fact: + info: "{{ result.stdout_lines }}" - debug: - var: info + var: info \ No newline at end of file diff --git a/apps/assets/automations/gather_accounts/manager.py b/apps/assets/automations/gather_accounts/manager.py index d6881f96c..40253be82 100644 --- a/apps/assets/automations/gather_accounts/manager.py +++ b/apps/assets/automations/gather_accounts/manager.py @@ -2,7 +2,7 @@ from common.utils import get_logger from assets.const import AutomationTypes from orgs.utils import tmp_to_org from .filter import GatherAccountsFilter -from ...models import Account, GatheredUser +from ...models import GatheredUser from ..base.manager import BasePlaybookManager logger = get_logger(__name__) @@ -42,4 +42,4 @@ class GatherAccountsManager(BasePlaybookManager): defaults['ip_last_login'] = data['address'][:32] GatheredUser.objects.update_or_create(defaults=defaults, asset=asset, username=username) else: - logger.error("Not found info, task name must be 'Get info': {}".format(host)) + logger.error("Not found info".format(host)) diff --git a/apps/assets/automations/gather_facts/database/postgresql/main.yml b/apps/assets/automations/gather_facts/database/postgresql/main.yml index 82adcdc16..98183e94c 100644 --- a/apps/assets/automations/gather_facts/database/postgresql/main.yml +++ b/apps/assets/automations/gather_facts/database/postgresql/main.yml @@ -10,7 +10,7 @@ login_password: "{{ jms_account.secret }}" login_host: "{{ jms_asset.address }}" login_port: "{{ jms_asset.port }}" - login_db: "{{ jms_asset.database }}" + login_db: "{{ jms_asset.category_property.db_name }}" register: db_info - name: Define info by set_fact diff --git a/apps/assets/automations/gather_facts/manager.py b/apps/assets/automations/gather_facts/manager.py index bbc3f9add..90fae1d75 100644 --- a/apps/assets/automations/gather_facts/manager.py +++ b/apps/assets/automations/gather_facts/manager.py @@ -26,4 +26,4 @@ class GatherFactsManager(BasePlaybookManager): asset.info = info asset.save() else: - logger.error("Not found info, task name must be 'Get info': {}".format(host)) + logger.error("Not found info: {}".format(host)) diff --git a/apps/assets/automations/ping/database/mysql/main.yml b/apps/assets/automations/ping/database/mysql/main.yml index fab498c76..ec7ca9432 100644 --- a/apps/assets/automations/ping/database/mysql/main.yml +++ b/apps/assets/automations/ping/database/mysql/main.yml @@ -2,12 +2,6 @@ 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 @@ -17,4 +11,3 @@ login_host: "{{ jms_asset.address }}" login_port: "{{ jms_asset.port }}" filter: version - register: db_info diff --git a/apps/assets/automations/ping/database/postgresql/main.yml b/apps/assets/automations/ping/database/postgresql/main.yml index 3bc2f7957..d76ba3ae3 100644 --- a/apps/assets/automations/ping/database/postgresql/main.yml +++ b/apps/assets/automations/ping/database/postgresql/main.yml @@ -2,16 +2,6 @@ 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 @@ -20,4 +10,4 @@ login_password: "{{ jms_account.secret }}" login_host: "{{ jms_asset.address }}" login_port: "{{ jms_asset.port }}" - login_db: "{{ jms_asset.database }}" + login_db: "{{ jms_asset.category_property.db_name }}" diff --git a/apps/assets/automations/ping/host/windows/main.yml b/apps/assets/automations/ping/host/windows/main.yml index 495b82a3d..d5af857a4 100644 --- a/apps/assets/automations/ping/host/windows/main.yml +++ b/apps/assets/automations/ping/host/windows/main.yml @@ -2,4 +2,4 @@ gather_facts: no tasks: - name: Windows ping - win_ping: + ansible.builtin.win_ping: diff --git a/apps/assets/automations/ping/manager.py b/apps/assets/automations/ping/manager.py index 84712fb44..791009e36 100644 --- a/apps/assets/automations/ping/manager.py +++ b/apps/assets/automations/ping/manager.py @@ -6,4 +6,15 @@ logger = get_logger(__name__) class PingManager(BasePlaybookManager): - pass + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.host_asset_mapper = {} + + @classmethod + def method_type(cls): + return AutomationTypes.ping + + def host_callback(self, host, asset=None, **kwargs): + super().host_callback(host, asset=asset, **kwargs) + self.host_asset_mapper[host['name']] = asset + return host diff --git a/apps/assets/automations/gather_facts/README.md b/apps/assets/automations/push_account/__init__.py similarity index 100% rename from apps/assets/automations/gather_facts/README.md rename to apps/assets/automations/push_account/__init__.py diff --git a/apps/assets/automations/push_account/database/mysql/main.yml b/apps/assets/automations/push_account/database/mysql/main.yml new file mode 100644 index 000000000..bf10c95af --- /dev/null +++ b/apps/assets/automations/push_account/database/mysql/main.yml @@ -0,0 +1,15 @@ +- hosts: mysql + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Add user account.username + 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: "%" diff --git a/apps/assets/automations/push_account/database/mysql/manifest.yml b/apps/assets/automations/push_account/database/mysql/manifest.yml new file mode 100644 index 000000000..d954cb1d8 --- /dev/null +++ b/apps/assets/automations/push_account/database/mysql/manifest.yml @@ -0,0 +1,6 @@ +id: push_account_mysql +name: Push account from MySQL +category: database +type: + - mysql +method: push_account diff --git a/apps/assets/automations/push_account/database/postgresql/main.yml b/apps/assets/automations/push_account/database/postgresql/main.yml new file mode 100644 index 000000000..72fbc5e83 --- /dev/null +++ b/apps/assets/automations/push_account/database/postgresql/main.yml @@ -0,0 +1,16 @@ +- hosts: postgresql + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Add user account.username + 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.category_property.db_name }}" + name: "{{ account.username }}" + password: "{{ account.secret }}" + diff --git a/apps/assets/automations/push_account/database/postgresql/manifest.yml b/apps/assets/automations/push_account/database/postgresql/manifest.yml new file mode 100644 index 000000000..6488ddd5a --- /dev/null +++ b/apps/assets/automations/push_account/database/postgresql/manifest.yml @@ -0,0 +1,6 @@ +id: push_account_postgresql +name: Push account for PostgreSQL +category: database +type: + - postgresql +method: push_account diff --git a/apps/assets/automations/push_account/host/posix/main.yml b/apps/assets/automations/push_account/host/posix/main.yml new file mode 100644 index 000000000..afe13b226 --- /dev/null +++ b/apps/assets/automations/push_account/host/posix/main.yml @@ -0,0 +1,19 @@ +- hosts: demo + gather_facts: no + tasks: + - name: Add user account.username + ansible.builtin.user: + name: "{{ account.username }}" + + - name: Set account.username password + ansible.builtin.user: + name: "{{ account.username }}" + password: "{{ account.secret | password_hash('sha512') }}" + update_password: always + when: secret_type == "password" + + - name: Set account.username SSH key + ansible.builtin.authorized_key: + user: "{{ account.username }}" + key: "{{ account.secret }}" + when: secret_type == "ssh_key" diff --git a/apps/assets/automations/push_account/host/posix/manifest.yml b/apps/assets/automations/push_account/host/posix/manifest.yml new file mode 100644 index 000000000..9a7cc5c8c --- /dev/null +++ b/apps/assets/automations/push_account/host/posix/manifest.yml @@ -0,0 +1,7 @@ +id: push_account_posix +name: Push posix account +category: host +type: + - linux + - unix +method: push_account diff --git a/apps/assets/automations/push_account/host/windows/main.yml b/apps/assets/automations/push_account/host/windows/main.yml new file mode 100644 index 000000000..bbe8219d0 --- /dev/null +++ b/apps/assets/automations/push_account/host/windows/main.yml @@ -0,0 +1,13 @@ +- hosts: windows + gather_facts: yes + tasks: + - name: Add user account.username + ansible.windows.win_user: + vars: + fullname: "{{ account.username }}" + name: "{{ account.username }}" + password: "{{ account.secret }}" + state: present + password_expired: no + update_password: always + password_never_expires: yes diff --git a/apps/assets/automations/push_account/host/windows/manifest.yml b/apps/assets/automations/push_account/host/windows/manifest.yml new file mode 100644 index 000000000..7e0256f44 --- /dev/null +++ b/apps/assets/automations/push_account/host/windows/manifest.yml @@ -0,0 +1,7 @@ +id: push_account_windows +name: Push account windows +version: 1 +method: push_account +category: host +type: + - windows diff --git a/apps/assets/automations/push_account/manager.py b/apps/assets/automations/push_account/manager.py new file mode 100644 index 000000000..ea5e2193f --- /dev/null +++ b/apps/assets/automations/push_account/manager.py @@ -0,0 +1,17 @@ +from common.utils import get_logger +from assets.const import AutomationTypes +from ..base.manager import BasePlaybookManager, PushOrVerifyHostCallbackMixin + +logger = get_logger(__name__) + + +class PushAccountManager(PushOrVerifyHostCallbackMixin, BasePlaybookManager): + ignore_account = True + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.host_account_mapper = {} + + @classmethod + def method_type(cls): + return AutomationTypes.push_account diff --git a/apps/assets/automations/verify_account/__init__.py b/apps/assets/automations/verify_account/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/assets/automations/verify_account/database/mysql/main.yml b/apps/assets/automations/verify_account/database/mysql/main.yml new file mode 100644 index 000000000..59c13d98a --- /dev/null +++ b/apps/assets/automations/verify_account/database/mysql/main.yml @@ -0,0 +1,13 @@ +- hosts: mysql + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Verify account + community.mysql.mysql_info: + login_user: "{{ account.username }}" + login_password: "{{ account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + filter: version diff --git a/apps/assets/automations/verify_account/database/mysql/manifest.yml b/apps/assets/automations/verify_account/database/mysql/manifest.yml new file mode 100644 index 000000000..a20b5c7d7 --- /dev/null +++ b/apps/assets/automations/verify_account/database/mysql/manifest.yml @@ -0,0 +1,6 @@ +id: verify_account_mysql +name: Verify account from MySQL +category: database +type: + - mysql +method: verify_account diff --git a/apps/assets/automations/verify_account/database/postgresql/main.yml b/apps/assets/automations/verify_account/database/postgresql/main.yml new file mode 100644 index 000000000..c70364767 --- /dev/null +++ b/apps/assets/automations/verify_account/database/postgresql/main.yml @@ -0,0 +1,13 @@ +- hosts: postgresql + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Verify account + 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.category_property.db_name }}" diff --git a/apps/assets/automations/verify_account/database/postgresql/manifest.yml b/apps/assets/automations/verify_account/database/postgresql/manifest.yml new file mode 100644 index 000000000..4c9e2cbec --- /dev/null +++ b/apps/assets/automations/verify_account/database/postgresql/manifest.yml @@ -0,0 +1,6 @@ +id: verify_account_postgresql +name: Verify account for PostgreSQL +category: database +type: + - postgresql +method: verify_account diff --git a/apps/assets/automations/verify_account/host/posix/main.yml b/apps/assets/automations/verify_account/host/posix/main.yml new file mode 100644 index 000000000..41ae1768d --- /dev/null +++ b/apps/assets/automations/verify_account/host/posix/main.yml @@ -0,0 +1,11 @@ +- hosts: demo + gather_facts: no + tasks: + - name: Verify account + ansible.builtin.ping: + become: no + vars: + ansible_user: "{{ account.username }}" + ansible_password: "{{ account.secret }}" + ansible_ssh_private_key_file: "{{ account.private_key_path }}" + ansible_become: no diff --git a/apps/assets/automations/verify_account/host/posix/manifest.yml b/apps/assets/automations/verify_account/host/posix/manifest.yml new file mode 100644 index 000000000..5b9a1e51b --- /dev/null +++ b/apps/assets/automations/verify_account/host/posix/manifest.yml @@ -0,0 +1,7 @@ +id: verify_account_posix +name: Verify posix account +category: host +type: + - linux + - unix +method: verify_account diff --git a/apps/assets/automations/verify_account/host/windows/main.yml b/apps/assets/automations/verify_account/host/windows/main.yml new file mode 100644 index 000000000..da9d40a74 --- /dev/null +++ b/apps/assets/automations/verify_account/host/windows/main.yml @@ -0,0 +1,8 @@ +- hosts: windows + gather_facts: yes + tasks: + - name: Verify account + ansible.windows.win_ping: + vars: + ansible_user: "{{ account.username }}" + ansible_password: "{{ account.secret }}" diff --git a/apps/assets/automations/verify_account/host/windows/manifest.yml b/apps/assets/automations/verify_account/host/windows/manifest.yml new file mode 100644 index 000000000..69faf4217 --- /dev/null +++ b/apps/assets/automations/verify_account/host/windows/manifest.yml @@ -0,0 +1,7 @@ +id: verify_account_windows +name: Verify account windows +version: 1 +method: verify_account +category: host +type: + - windows diff --git a/apps/assets/automations/verify_account/manager.py b/apps/assets/automations/verify_account/manager.py new file mode 100644 index 000000000..5445511ba --- /dev/null +++ b/apps/assets/automations/verify_account/manager.py @@ -0,0 +1,25 @@ +from common.utils import get_logger +from assets.const import AutomationTypes, Connectivity +from ..base.manager import BasePlaybookManager, PushOrVerifyHostCallbackMixin + +logger = get_logger(__name__) + + +class VerifyAccountManager(PushOrVerifyHostCallbackMixin, BasePlaybookManager): + ignore_account = False + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.host_account_mapper = {} + + @classmethod + def method_type(cls): + return AutomationTypes.verify_account + + def on_host_success(self, host, result): + account = self.host_account_mapper.get(host) + account.set_connectivity(Connectivity.ok) + + def on_host_error(self, host, error, result): + account = self.host_account_mapper.get(host) + account.set_connectivity(Connectivity.failed) diff --git a/apps/assets/migrations/0107_auto_20221019_1115.py b/apps/assets/migrations/0107_auto_20221019_1115.py index c720655a8..384178376 100644 --- a/apps/assets/migrations/0107_auto_20221019_1115.py +++ b/apps/assets/migrations/0107_auto_20221019_1115.py @@ -111,7 +111,7 @@ class Migration(migrations.Migration): ('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': 'Push automation', + 'verbose_name': 'Push asset account', }, bases=('assets.baseautomation',), ), @@ -121,7 +121,7 @@ class Migration(migrations.Migration): ('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 account automation', + 'verbose_name': 'Verify asset account', }, bases=('assets.baseautomation',), ), diff --git a/apps/assets/models/automations/__init__.py b/apps/assets/models/automations/__init__.py index 1c62bbddd..5c2a3e031 100644 --- a/apps/assets/models/automations/__init__.py +++ b/apps/assets/models/automations/__init__.py @@ -1,6 +1,6 @@ from .change_secret import * from .discovery_account import * from .push_account import * -from .verify_secret import * from .gather_facts import * from .gather_accounts import * +from .verify_account import * diff --git a/apps/assets/models/automations/change_secret.py b/apps/assets/models/automations/change_secret.py index ef20e27b0..53ca08aba 100644 --- a/apps/assets/models/automations/change_secret.py +++ b/apps/assets/models/automations/change_secret.py @@ -2,7 +2,6 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from common.db import fields -from common.const.choices import Trigger from common.db.models import JMSBaseModel from assets.const import AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy from .base import BaseAutomation diff --git a/apps/assets/models/automations/push_account.py b/apps/assets/models/automations/push_account.py index c36d89b43..b1da1966f 100644 --- a/apps/assets/models/automations/push_account.py +++ b/apps/assets/models/automations/push_account.py @@ -1,15 +1,16 @@ from django.utils.translation import ugettext_lazy as _ +from assets.const import AutomationTypes from .base import BaseAutomation +__all__ = ['PushAccountAutomation'] + class PushAccountAutomation(BaseAutomation): - class Meta: - verbose_name = _("Push automation") - def to_attr_json(self): - attr_json = super().to_attr_json() - attr_json.update({ - 'type': 'push_account' - }) - return attr_json + def save(self, *args, **kwargs): + self.type = AutomationTypes.verify_account + super().save(*args, **kwargs) + + class Meta: + verbose_name = _("Push asset account") diff --git a/apps/assets/models/automations/verify_secret.py b/apps/assets/models/automations/verify_account.py similarity index 56% rename from apps/assets/models/automations/verify_secret.py rename to apps/assets/models/automations/verify_account.py index 62326c8bb..cf7004820 100644 --- a/apps/assets/models/automations/verify_secret.py +++ b/apps/assets/models/automations/verify_account.py @@ -1,12 +1,15 @@ from django.utils.translation import ugettext_lazy as _ +from assets.const import AutomationTypes from .base import BaseAutomation +__all__ = ['VerifyAccountAutomation'] + class VerifyAccountAutomation(BaseAutomation): - class Meta: - verbose_name = _("Verify account automation") - def save(self, *args, **kwargs): - self.type = 'verify_account' + self.type = AutomationTypes.verify_account super().save(*args, **kwargs) + + class Meta: + verbose_name = _("Verify asset account") diff --git a/apps/ops/ansible/callback.py b/apps/ops/ansible/callback.py index 344605a18..c613d5080 100644 --- a/apps/ops/ansible/callback.py +++ b/apps/ops/ansible/callback.py @@ -49,7 +49,7 @@ class DefaultCallback: } self.result['ok'][host][task] = detail - def runer_on_failed(self, event_data, host=None, task=None, res=None, **kwargs): + def runner_on_failed(self, event_data, host=None, task=None, res=None, **kwargs): detail = { 'action': event_data.get('task_action', ''), 'res': res, diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py index 35344ad6a..8a1ae0634 100644 --- a/apps/ops/ansible/inventory.py +++ b/apps/ops/ansible/inventory.py @@ -9,16 +9,18 @@ __all__ = ['JMSInventory'] class JMSInventory: - def __init__(self, manager, assets=None, account_policy='smart', account_prefer='root,administrator'): + def __init__(self, assets, account_policy='smart', + account_prefer='root,administrator', + host_callback=None): """ :param assets: :param account_prefer: account username name if not set use account_policy :param account_policy: smart, privileged_must, privileged_first """ - self.manager = manager self.assets = self.clean_assets(assets) self.account_prefer = account_prefer self.account_policy = account_policy + self.host_callback = host_callback @staticmethod def clean_assets(assets): @@ -61,7 +63,6 @@ class JMSInventory: var = { 'ansible_user': account.username, } - if not account.secret: return var if account.secret_type == 'password': @@ -78,10 +79,7 @@ class JMSInventory: ssh_protocol_matched = list(filter(lambda x: x.name == 'ssh', protocols)) ssh_protocol = ssh_protocol_matched[0] if ssh_protocol_matched else None host['ansible_host'] = asset.address - if asset.port == 0: - host['ansible_port'] = ssh_protocol.port if ssh_protocol else 22 - else: - host['ansible_port'] = asset.port + host['ansible_port'] = ssh_protocol.port if ssh_protocol else 22 su_from = account.su_from if platform.su_enabled and su_from: @@ -106,7 +104,8 @@ class JMSInventory: 'jms_asset': { 'id': str(asset.id), 'name': asset.name, 'address': asset.address, 'type': asset.type, 'category': asset.category, - 'protocol': asset.protocol, 'port': asset.port,'database': '', + 'protocol': asset.protocol, 'port': asset.port, + 'category_property': asset.category_property, 'protocols': [{'name': p.name, 'port': p.port} for p in protocols], }, 'jms_account': { @@ -118,9 +117,6 @@ class JMSInventory: ansible_connection = ansible_config.get('ansible_connection', 'ssh') host.update(ansible_config) - if platform.category == 'database': - host['jms_asset']['database'] = asset.database.db_name - gateway = None if asset.domain: gateway = asset.domain.select_gateway() @@ -167,17 +163,17 @@ class JMSInventory: platform_assets = self.group_by_platform(self.assets) for platform, assets in platform_assets.items(): automation = platform.automation - protocols = platform.protocols.all() for asset in assets: + protocols = asset.protocols.all() account = self.select_account(asset) host = self.asset_to_host(asset, account, automation, protocols, platform) if not automation.ansible_enabled: host['error'] = _('Ansible disabled') - if self.manager.host_callback is not None: - host = self.manager.host_callback( + if self.host_callback is not None: + host = self.host_callback( host, asset=asset, account=account, platform=platform, automation=automation, path_dir=path_dir diff --git a/apps/ops/api/celery.py b/apps/ops/api/celery.py index 85a5c00d2..61d13db17 100644 --- a/apps/ops/api/celery.py +++ b/apps/ops/api/celery.py @@ -99,7 +99,7 @@ class CeleryPeriodTaskViewSet(CommonApiMixin, viewsets.ModelViewSet): class CeleryTaskViewSet(CommonApiMixin, viewsets.ReadOnlyModelViewSet): - queryset = CeleryTask.objects.filter(name__in=['ops.tasks.hello', 'ops.tasks.hello_error', 'ops.tasks.hello_random']) + queryset = CeleryTask.objects.all() serializer_class = CeleryTaskSerializer http_method_names = ('get', 'head', 'options',) diff --git a/apps/terminal/api/applet/applet.py b/apps/terminal/api/applet/applet.py index 9ded63dc1..bce0738d6 100644 --- a/apps/terminal/api/applet/applet.py +++ b/apps/terminal/api/applet/applet.py @@ -1,7 +1,7 @@ -import os.path import shutil import zipfile import yaml +import os.path from django.core.files.storage import default_storage from rest_framework import viewsets @@ -9,12 +9,16 @@ from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.serializers import ValidationError -from terminal import serializers, models +from terminal import serializers +from terminal.models import AppletPublication, Applet from terminal.serializers import AppletUploadSerializer +__all__ = ['AppletViewSet', 'AppletPublicationViewSet'] + + class AppletViewSet(viewsets.ModelViewSet): - queryset = models.Applet.objects.all() + queryset = Applet.objects.all() serializer_class = serializers.AppletSerializer rbac_perms = { 'upload': 'terminal.add_applet', @@ -67,7 +71,7 @@ class AppletViewSet(viewsets.ModelViewSet): name = manifest['name'] update = request.query_params.get('update') - instance = models.Applet.objects.filter(name=name).first() + instance = Applet.objects.filter(name=name).first() if instance and not update: return Response({'error': 'Applet already exists: {}'.format(name)}, status=400) @@ -82,5 +86,5 @@ class AppletViewSet(viewsets.ModelViewSet): class AppletPublicationViewSet(viewsets.ModelViewSet): - queryset = models.AppletPublication.objects.all() + queryset = AppletPublication.objects.all() serializer_class = serializers.AppletPublicationSerializer diff --git a/apps/terminal/api/applet/host.py b/apps/terminal/api/applet/host.py index d4166ea98..ea8831b8f 100644 --- a/apps/terminal/api/applet/host.py +++ b/apps/terminal/api/applet/host.py @@ -1,23 +1,35 @@ from rest_framework import viewsets +from rest_framework.decorators import action +from rest_framework.response import Response from orgs.utils import tmp_to_builtin_org -from terminal import serializers, models +from terminal import serializers +from terminal.models import AppletHost, Applet +from terminal.tasks import run_applet_host_deployment -__all__ = ['AppletHostViewSet', 'AppletHostDeploymentViewSet'] +__all__ = ['AppletHostViewSet'] class AppletHostViewSet(viewsets.ModelViewSet): serializer_class = serializers.AppletHostSerializer def get_queryset(self): - return models.AppletHost.objects.all() + return AppletHost.objects.all() def dispatch(self, request, *args, **kwargs): with tmp_to_builtin_org(system=1): return super().dispatch(request, *args, **kwargs) + @action(methods=['post'], detail=True) + def deploy(self, request): + from terminal.automations.deploy_applet_host.manager import DeployAppletHostManager + manager = DeployAppletHostManager(self) + manager.run() -class AppletHostDeploymentViewSet(viewsets.ModelViewSet): - queryset = models.AppletHostDeployment.objects.all() - serializer_class = serializers.AppletHostDeploymentSerializer + @action(methods=['get'], detail=True, url_path='') + def not_published_applets(self, request, *args, **kwargs): + instance = self.get_object() + applets = Applet.objects.exclude(id__in=instance.applets.all()) + serializer = serializers.AppletSerializer(applets, many=True) + return Response(serializer.data) diff --git a/apps/terminal/automations/deploy_applet_host/manager.py b/apps/terminal/automations/deploy_applet_host/manager.py index e69de29bb..22a8510af 100644 --- a/apps/terminal/automations/deploy_applet_host/manager.py +++ b/apps/terminal/automations/deploy_applet_host/manager.py @@ -0,0 +1,43 @@ +import os +import datetime +import shutil +from django.conf import settings + +from ops.ansible import PlaybookRunner, JMSInventory + +CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class DeployAppletHostManager: + def __init__(self, applet_host): + self.applet_host = applet_host + self.run_dir = self.get_run_dir() + + @staticmethod + def get_run_dir(): + base = os.path.join(settings.ANSIBLE_DIR, 'applet_host_deploy') + now = datetime.datetime.now().strftime('%Y%m%d%H%M%S') + return os.path.join(base, now) + + def generate_playbook(self): + playbook_src = os.path.join(CURRENT_DIR, 'playbook.yml') + playbook_dir = os.path.join(self.run_dir, 'playbook') + playbook_dst = os.path.join(playbook_dir, 'main.yml') + os.makedirs(playbook_dir, exist_ok=True) + shutil.copy(playbook_src, playbook_dst) + return playbook_dst + + def generate_inventory(self): + inventory = JMSInventory([self.applet_host], account_policy='privileged_only') + inventory_dir = os.path.join(self.run_dir, 'inventory') + inventory_path = os.path.join(inventory_dir, 'hosts.yml') + inventory.write_to_file(inventory_path) + return inventory_path + + def run(self, **kwargs): + inventory = self.generate_inventory() + playbook = self.generate_playbook() + runner = PlaybookRunner( + inventory=inventory, playbook=playbook, project_dir=self.run_dir + ) + return runner.run(**kwargs) diff --git a/apps/terminal/automations/deploy_applet_host/playbook.yml b/apps/terminal/automations/deploy_applet_host/playbook.yml index 17016328d..20970c952 100644 --- a/apps/terminal/automations/deploy_applet_host/playbook.yml +++ b/apps/terminal/automations/deploy_applet_host/playbook.yml @@ -1,56 +1,138 @@ --- -- hosts: windows + +- hosts: all vars: - DownloadHost: https://demo.jumpserver.org/download - RDS_Licensing: enabled - RDS_LicenseServer: 127.0.0.1 - RDS_LicensingMode: 4 - - RDS_fSingleSessionPerUser: 0 + - RDS_fSingleSessionPerUser: 1 - RDS_MaxDisconnectionTime: 60000 - RDS_RemoteAppLogoffTimeLimit: 0 + tasks: - - name: Install RDS-Licensing (RDS) - ansible.windows.win_feature: - name: RDS-Licensing - state: present - include_management_tools: yes - when: RDS_Licensing == "enabled" - - name: Install RDS-RD-Server (RDS) - ansible.windows.win_feature: - name: RDS-RD-Server - state: present - include_management_tools: yes - register: win_feature - - name: Reboot if installing RDS feature requires it - ansible.windows.win_reboot: - when: win_feature.reboot_required - - name: Set RDS LicenseServer (regedit) - ansible.windows.win_regedit: - path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services - name: LicenseServers - data: "{{ RDS_LicenseServer }}" - type: string - - name: Set RDS LicensingMode (regedit) - ansible.windows.win_regedit: - path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services - name: LicensingMode - data: "{{ RDS_LicensingMode }}" - type: dword - - name: Set RDS fSingleSessionPerUser (regedit) - ansible.windows.win_regedit: - path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services - name: fSingleSessionPerUser - data: "{{ RDS_fSingleSessionPerUser }}" - type: dword - - name: Set RDS MaxDisconnectionTime (regedit) - ansible.windows.win_regedit: - path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services - name: MaxDisconnectionTime - data: "{{ RDS_MaxDisconnectionTime }}" - type: dword - when: RDS_MaxDisconnectionTime >= 60000 - - name: Set RDS RemoteAppLogoffTimeLimit (regedit) - ansible.windows.win_regedit: - path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services - name: RemoteAppLogoffTimeLimit - data: "{{ RDS_RemoteAppLogoffTime }}" + - name: Install RDS-Licensing (RDS) + ansible.windows.win_feature: + name: RDS-Licensing + state: present + include_management_tools: yes + when: RDS_Licensing == "enabled" + + - name: Install RDS-RD-Server (RDS) + ansible.windows.win_feature: + name: RDS-RD-Server + state: present + include_management_tools: yes + register: rds_install + + - name: Download Jmservisor (jumpserver) + ansible.windows.win_get_url: + url: "{{ DownloadHost }}/Jmservisor.msi" + dest: "{{ ansible_env.TEMP }}\\Jmservisor.msi" + + - name: Install the Jmservisor (jumpserver) + ansible.windows.win_package: + path: "{{ ansible_env.TEMP }}\\Jmservisor.msi" + state: present + + - name: Download python-3.10.8 + ansible.windows.win_get_url: + url: "{{ DownloadHost }}/python-3.10.8-amd64.exe" + dest: "{{ ansible_env.TEMP }}\\python-3.10.8-amd64.exe" + + - name: Install the python-3.10.8 + ansible.windows.win_package: + path: "{{ ansible_env.TEMP }}\\python-3.10.8-amd64.exe" + product_id: '{371d0d73-d418-4ffe-b280-58c3e7987525}' + arguments: + - /quiet + - InstallAllUsers=1 + - PrependPath=1 + - Include_test=0 + - Include_launcher=0 + state: present + register: win_install_python + + - name: Reboot if installing requires it + ansible.windows.win_reboot: + post_reboot_delay: 10 + test_command: whoami + when: rds_install.reboot_required or win_install_python.reboot_required + + - name: Set RDS LicenseServer (regedit) + ansible.windows.win_regedit: + path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services + name: LicenseServers + data: "{{ RDS_LicenseServer }}" + type: string + + - name: Set RDS LicensingMode (regedit) + ansible.windows.win_regedit: + path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services + name: LicensingMode + data: "{{ RDS_LicensingMode }}" + type: dword + + - name: Set RDS fSingleSessionPerUser (regedit) + ansible.windows.win_regedit: + path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services + name: fSingleSessionPerUser + data: "{{ RDS_fSingleSessionPerUser }}" + type: dword + + - name: Set RDS MaxDisconnectionTime (regedit) + ansible.windows.win_regedit: + path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services + name: MaxDisconnectionTime + data: "{{ RDS_MaxDisconnectionTime }}" + type: dword + when: RDS_MaxDisconnectionTime >= 60000 + + - name: Set RDS RemoteAppLogoffTimeLimit (regedit) + ansible.windows.win_regedit: + path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services + name: RemoteAppLogoffTimeLimit + data: "{{ RDS_RemoteAppLogoffTimeLimit }}" + type: dword + + - name: Download pip packages + ansible.windows.win_get_url: + url: "{{ DownloadHost }}/pip_packages_v0.0.1.zip" + dest: "{{ ansible_env.TEMP }}\\pip_packages_v0.0.1.zip" + + - name: Unzip pip_packages + community.windows.win_unzip: + src: "{{ ansible_env.TEMP }}\\pip_packages_v0.0.1.zip" + dest: "{{ ansible_env.TEMP }}" + + - name: Install python requirements offline + ansible.windows.win_shell: > + pip install -r '{{ ansible_env.TEMP }}\pip_packages_v0.0.1\requirements.txt' + --no-index --find-links='{{ ansible_env.TEMP }}\pip_packages_v0.0.1' + + - name: Download chromedriver (chrome) + ansible.windows.win_get_url: + url: "{{ DownloadHost }}/chromedriver_win32.106.zip" + dest: "{{ ansible_env.TEMP }}\\chromedriver_win32.106.zip" + + - name: Unzip chromedriver (chrome) + community.windows.win_unzip: + src: "{{ ansible_env.TEMP }}\\chromedriver_win32.106.zip" + dest: C:\Program Files\JumpServer\drivers + + - name: Set chromedriver on the global system path (chrome) + ansible.windows.win_path: + elements: + - 'C:\Program Files\JumpServer\drivers' + + - name: Download chrome msi package (chrome) + ansible.windows.win_get_url: + url: "{{ DownloadHost }}/googlechromestandaloneenterprise64.msi" + dest: "{{ ansible_env.TEMP }}\\googlechromestandaloneenterprise64.msi" + + - name: Install chrome (chrome) + ansible.windows.win_package: + path: "{{ ansible_env.TEMP }}\\googlechromestandaloneenterprise64.msi" + state: present + arguments: + - /quiet diff --git a/apps/terminal/migrations/0055_auto_20221028_1544.py b/apps/terminal/migrations/0055_auto_20221028_1544.py new file mode 100644 index 000000000..3aeeff3f6 --- /dev/null +++ b/apps/terminal/migrations/0055_auto_20221028_1544.py @@ -0,0 +1,42 @@ +# Generated by Django 3.2.14 on 2022-10-28 07:44 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0054_auto_20221027_1125'), + ] + + operations = [ + migrations.AddField( + model_name='applet', + name='hosts', + field=models.ManyToManyField(through='terminal.AppletPublication', to='terminal.AppletHost', verbose_name='Hosts'), + ), + migrations.AddField( + model_name='applethost', + name='date_inited', + field=models.DateTimeField(blank=True, null=True, verbose_name='Date initialized'), + ), + migrations.AddField( + model_name='applethost', + name='initialized', + field=models.BooleanField(default=False, verbose_name='Initialized'), + ), + migrations.AlterField( + model_name='appletpublication', + name='applet', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='publications', to='terminal.applet', verbose_name='Applet'), + ), + migrations.AlterField( + model_name='appletpublication', + name='host', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='publications', to='terminal.applethost', verbose_name='Host'), + ), + migrations.DeleteModel( + name='AppletHostDeployment', + ), + ] diff --git a/apps/terminal/models/applet/applet.py b/apps/terminal/models/applet/applet.py index 0969204b7..9dde129e7 100644 --- a/apps/terminal/models/applet/applet.py +++ b/apps/terminal/models/applet/applet.py @@ -26,6 +26,7 @@ class Applet(JMSBaseModel): protocols = models.JSONField(default=list, verbose_name=_('Protocol')) tags = models.JSONField(default=list, verbose_name=_('Tags')) comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) + hosts = models.ManyToManyField(through_fields=('applet', 'host'), through='AppletPublication', to='AppletHost', verbose_name=_('Hosts')) def __str__(self): return self.name @@ -51,8 +52,8 @@ class Applet(JMSBaseModel): class AppletPublication(JMSBaseModel): - applet = models.ForeignKey('Applet', on_delete=models.PROTECT, verbose_name=_('Applet')) - host = models.ForeignKey('AppletHost', on_delete=models.PROTECT, verbose_name=_('Host')) + applet = models.ForeignKey('Applet', on_delete=models.PROTECT, related_name='publications', verbose_name=_('Applet')) + host = models.ForeignKey('AppletHost', on_delete=models.PROTECT, related_name='publications', verbose_name=_('Host')) status = models.CharField(max_length=16, verbose_name=_('Status')) comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) diff --git a/apps/terminal/models/applet/host.py b/apps/terminal/models/applet/host.py index 2fee8d15d..7733e7b0f 100644 --- a/apps/terminal/models/applet/host.py +++ b/apps/terminal/models/applet/host.py @@ -1,15 +1,17 @@ from django.db import models from django.utils.translation import gettext_lazy as _ -from common.db.models import JMSBaseModel from assets.models import Host +from ops.ansible import PlaybookRunner, JMSInventory -__all__ = ['AppletHost', 'AppletHostDeployment'] +__all__ = ['AppletHost'] class AppletHost(Host): account_automation = models.BooleanField(default=False, verbose_name=_('Account automation')) + initialized = models.BooleanField(default=False, verbose_name=_('Initialized')) + date_inited = models.DateTimeField(null=True, blank=True, verbose_name=_('Date initialized')) date_synced = models.DateTimeField(null=True, blank=True, verbose_name=_('Date synced')) status = models.CharField(max_length=16, verbose_name=_('Status')) applets = models.ManyToManyField( @@ -17,17 +19,11 @@ class AppletHost(Host): through='AppletPublication', through_fields=('host', 'applet'), ) + def deploy(self): + inventory = JMSInventory([self]) + playbook = PlaybookRunner(inventory, 'applets.yml') + playbook.run() + def __str__(self): return self.name - -class AppletHostDeployment(JMSBaseModel): - host = models.ForeignKey('AppletHost', on_delete=models.CASCADE, verbose_name=_('Hosting')) - status = models.CharField(max_length=16, verbose_name=_('Status')) - comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) - - def __str__(self): - return self.host - - def start(self): - pass diff --git a/apps/terminal/serializers/applet.py b/apps/terminal/serializers/applet.py index 97f8fae64..802b0a304 100644 --- a/apps/terminal/serializers/applet.py +++ b/apps/terminal/serializers/applet.py @@ -5,12 +5,12 @@ from common.drf.fields import ObjectRelatedField, LabeledChoiceField from common.validators import ProjectUniqueValidator from assets.models import Platform from assets.serializers import HostSerializer -from ..models import Applet, AppletPublication, AppletHost, AppletHostDeployment +from ..models import Applet, AppletPublication, AppletHost __all__ = [ 'AppletSerializer', 'AppletPublicationSerializer', - 'AppletHostSerializer', 'AppletHostDeploymentSerializer', + 'AppletHostSerializer', 'AppletUploadSerializer' ] @@ -85,14 +85,3 @@ class AppletHostSerializer(HostSerializer): validators.append(uniq_validator) return validators - -class AppletHostDeploymentSerializer(serializers.ModelSerializer): - host = ObjectRelatedField(queryset=AppletHost.objects.all()) - - class Meta: - model = AppletHostDeployment - fields_mini = ['id', 'host'] - read_only_fields = ['date_created', 'date_updated'] - fields = fields_mini + [ - 'status', 'comment', - ] + read_only_fields diff --git a/apps/terminal/startup.py b/apps/terminal/startup.py index 7e454da83..672d31830 100644 --- a/apps/terminal/startup.py +++ b/apps/terminal/startup.py @@ -11,7 +11,7 @@ from common.utils import get_disk_usage, get_cpu_load, get_memory_usage, get_log from .serializers.terminal import TerminalRegistrationSerializer, StatusSerializer from .const import TerminalTypeChoices -from .models.terminal import Terminal +from .models import Terminal __all__ = ['CoreTerminal', 'CeleryTerminal'] diff --git a/apps/terminal/tasks.py b/apps/terminal/tasks.py index 59cf00fa4..e05d673e5 100644 --- a/apps/terminal/tasks.py +++ b/apps/terminal/tasks.py @@ -14,7 +14,7 @@ from common.utils import get_log_keep_day from ops.celery.decorator import ( register_as_period_task, after_app_ready_start, after_app_shutdown_clean_periodic ) -from .models import Status, Session, Command, Task +from .models import Status, Session, Command, Task, AppletHost from .backends import server_replay_storage from .utils import find_session_replay_local @@ -99,3 +99,9 @@ def upload_session_replay_to_external_storage(session_id): except: pass return + + +@shared_task +def run_applet_host_deployment(did): + host = AppletHost.objects.get(id=did) + host.deploy() diff --git a/apps/terminal/urls/api_urls.py b/apps/terminal/urls/api_urls.py index 8bed4f604..1d369cc52 100644 --- a/apps/terminal/urls/api_urls.py +++ b/apps/terminal/urls/api_urls.py @@ -26,8 +26,7 @@ router.register(r'endpoints', api.EndpointViewSet, 'endpoint') router.register(r'endpoint-rules', api.EndpointRuleViewSet, 'endpoint-rule') router.register(r'applets', api.AppletViewSet, 'applet') router.register(r'applet-hosts', api.AppletHostViewSet, 'applet-host') -router.register(r'applet-publication', api.AppletPublicationViewSet, 'applet-publication') -router.register(r'applet-host-deployment', api.AppletHostDeploymentViewSet, 'applet-host-deployment') +router.register(r'applet-publications', api.AppletPublicationViewSet, 'applet-publication') urlpatterns = [ @@ -46,10 +45,6 @@ urlpatterns = [ path('command-storages//test-connective/', api.CommandStorageTestConnectiveApi.as_view(), name='command-storage-test-connective'), # components path('components/metrics/', api.ComponentsMetricsAPIView.as_view(), name='components-metrics'), - # v2: get session's replay - # path('v2/sessions//replay/', - # api.SessionReplayV2ViewSet.as_view({'get': 'retrieve'}), - # name='session-replay-v2'), ] old_version_urlpatterns = [