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

pull/9008/head
Jiangjie.Bai 2022-10-31 18:50:14 +08:00
commit a5244ee68f
58 changed files with 615 additions and 263 deletions

View File

@ -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 = {

View File

@ -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)))

View File

@ -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

View File

@ -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"

View File

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

View File

@ -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']))

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -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 }}"

View File

@ -2,4 +2,4 @@
gather_facts: no
tasks:
- name: Windows ping
win_ping:
ansible.builtin.win_ping:

View File

@ -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

View File

@ -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: "%"

View File

@ -0,0 +1,6 @@
id: push_account_mysql
name: Push account from MySQL
category: database
type:
- mysql
method: push_account

View File

@ -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 }}"

View File

@ -0,0 +1,6 @@
id: push_account_postgresql
name: Push account for PostgreSQL
category: database
type:
- postgresql
method: push_account

View File

@ -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"

View File

@ -0,0 +1,7 @@
id: push_account_posix
name: Push posix account
category: host
type:
- linux
- unix
method: push_account

View File

@ -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

View File

@ -0,0 +1,7 @@
id: push_account_windows
name: Push account windows
version: 1
method: push_account
category: host
type:
- windows

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,6 @@
id: verify_account_mysql
name: Verify account from MySQL
category: database
type:
- mysql
method: verify_account

View File

@ -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 }}"

View File

@ -0,0 +1,6 @@
id: verify_account_postgresql
name: Verify account for PostgreSQL
category: database
type:
- postgresql
method: verify_account

View File

@ -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

View File

@ -0,0 +1,7 @@
id: verify_account_posix
name: Verify posix account
category: host
type:
- linux
- unix
method: verify_account

View File

@ -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 }}"

View File

@ -0,0 +1,7 @@
id: verify_account_windows
name: Verify account windows
version: 1
method: verify_account
category: host
type:
- windows

View File

@ -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)

View File

@ -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',),
),

View File

@ -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 *

View File

@ -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

View File

@ -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")

View File

@ -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")

View File

@ -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,

View File

@ -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

View File

@ -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',)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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',
),
]

View File

@ -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'))

View File

@ -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

View File

@ -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

View File

@ -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']

View File

@ -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()

View File

@ -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/<uuid:pk>/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/<uuid:pk>/replay/',
# api.SessionReplayV2ViewSet.as_view({'get': 'retrieve'}),
# name='session-replay-v2'),
]
old_version_urlpatterns = [