diff --git a/apps/accounts/api/automations/gather_accounts.py b/apps/accounts/api/automations/gather_accounts.py
index e125ed96b..6c669d800 100644
--- a/apps/accounts/api/automations/gather_accounts.py
+++ b/apps/accounts/api/automations/gather_accounts.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
#
+from django.shortcuts import get_object_or_404
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response
@@ -7,8 +8,9 @@ from rest_framework.response import Response
from accounts import serializers
from accounts.const import AutomationTypes
from accounts.filters import GatheredAccountFilterSet
-from accounts.models import GatherAccountsAutomation
+from accounts.models import GatherAccountsAutomation, AutomationExecution
from accounts.models import GatheredAccount
+from assets.models import Asset
from orgs.mixins.api import OrgBulkModelViewSet
from .base import AutomationExecutionViewSet
@@ -49,8 +51,29 @@ class GatheredAccountViewSet(OrgBulkModelViewSet):
}
rbac_perms = {
'sync_accounts': 'assets.add_gatheredaccount',
+ 'discover': 'assets.add_gatheredaccount',
}
+ @action(methods=['get'], detail=False, url_path='discover')
+ def discover(self, request, *args, **kwargs):
+ asset_id = request.query_params.get('asset_id')
+ if not asset_id:
+ return Response(status=status.HTTP_400_BAD_REQUEST, data={'asset_id': 'This field is required.'})
+ asset = get_object_or_404(Asset, pk=asset_id)
+ execution = AutomationExecution()
+ execution.snapshot = {
+ 'assets': [asset_id],
+ 'nodes': [],
+ 'type': 'gather_accounts',
+ 'is_sync_account': True,
+ 'name': 'Adhoc gather accounts: {}'.format(asset_id),
+ }
+ execution.save()
+ execution.start()
+ accounts = self.model.objects.filter(asset=asset)
+ serializer = self.get_serializer(accounts, many=True)
+ return Response(status=status.HTTP_200_OK, data=serializer.data)
+
@action(methods=['post'], detail=False, url_path='sync-accounts')
def sync_accounts(self, request, *args, **kwargs):
gathered_account_ids = request.data.get('gathered_account_ids')
diff --git a/apps/accounts/automations/gather_accounts/manager.py b/apps/accounts/automations/gather_accounts/manager.py
index e97d78cd0..c78b3b891 100644
--- a/apps/accounts/automations/gather_accounts/manager.py
+++ b/apps/accounts/automations/gather_accounts/manager.py
@@ -3,6 +3,7 @@ from collections import defaultdict
from accounts.const import AutomationTypes
from accounts.models import GatheredAccount
from assets.models import Asset
+from common.const import ConfirmOrIgnore
from common.utils import get_logger
from orgs.utils import tmp_to_org
from users.models import User
@@ -70,8 +71,9 @@ class GatherAccountsManager(AccountBasePlaybookManager):
def update_or_create_accounts(self):
for asset, data in self.asset_account_info.items():
- with tmp_to_org(asset.org_id):
+ with (tmp_to_org(asset.org_id)):
gathered_accounts = []
+ # 把所有的设置为 present = False, 创建的时候如果有就会更新
GatheredAccount.objects.filter(asset=asset, present=True).update(present=False)
for d in data:
username = d['username']
@@ -79,10 +81,16 @@ class GatherAccountsManager(AccountBasePlaybookManager):
defaults=d, asset=asset, username=username,
)
gathered_accounts.append(gathered_account)
+ # 不存在的标识为待处理
+ GatheredAccount.objects \
+ .filter(asset=asset, present=False) \
+ .exclude(status=ConfirmOrIgnore.ignored) \
+ .update(status='')
if not self.is_sync_account:
continue
GatheredAccount.sync_accounts(gathered_accounts)
+
def run(self, *args, **kwargs):
super().run(*args, **kwargs)
users, change_info = self.generate_send_users_and_change_info()
diff --git a/apps/accounts/migrations/0010_alter_accountrisk_options_and_more.py b/apps/accounts/migrations/0010_alter_accountrisk_options_and_more.py
new file mode 100644
index 000000000..afbde0723
--- /dev/null
+++ b/apps/accounts/migrations/0010_alter_accountrisk_options_and_more.py
@@ -0,0 +1,41 @@
+# Generated by Django 4.1.13 on 2024-10-28 08:05
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("accounts", "0009_remove_account_date_discovery_and_more"),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name="accountrisk",
+ options={"verbose_name": "Account risk"},
+ ),
+ migrations.RenameField(
+ model_name="account",
+ old_name="date_last_access",
+ new_name="date_last_login",
+ ),
+ migrations.RenameField(
+ model_name="account",
+ old_name="access_by",
+ new_name="login_by",
+ ),
+ migrations.AddField(
+ model_name="gatheredaccount",
+ name="action",
+ field=models.CharField(
+ choices=[
+ ("pending", "Pending"),
+ ("confirm", "Confirm"),
+ ("ignore", "Ignore"),
+ ],
+ default="pending",
+ max_length=32,
+ verbose_name="Action",
+ ),
+ ),
+ ]
diff --git a/apps/accounts/migrations/0011_remove_gatheredaccount_action_gatheredaccount_status.py b/apps/accounts/migrations/0011_remove_gatheredaccount_action_gatheredaccount_status.py
new file mode 100644
index 000000000..d057654ff
--- /dev/null
+++ b/apps/accounts/migrations/0011_remove_gatheredaccount_action_gatheredaccount_status.py
@@ -0,0 +1,28 @@
+# Generated by Django 4.1.13 on 2024-10-28 08:19
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("accounts", "0010_alter_accountrisk_options_and_more"),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name="gatheredaccount",
+ name="action",
+ ),
+ migrations.AddField(
+ model_name="gatheredaccount",
+ name="status",
+ field=models.CharField(
+ blank=True,
+ choices=[("confirmed", "Confirmed"), ("ignored", "Ignored")],
+ default="",
+ max_length=32,
+ verbose_name="Action",
+ ),
+ ),
+ ]
diff --git a/apps/accounts/models/account.py b/apps/accounts/models/account.py
index 70c4b537c..419ed4f8b 100644
--- a/apps/accounts/models/account.py
+++ b/apps/accounts/models/account.py
@@ -57,8 +57,8 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount):
history = AccountHistoricalRecords(included_fields=['id', '_secret', 'secret_type', 'version'])
source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source'))
source_id = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Source ID'))
- date_last_access = models.DateTimeField(null=True, blank=True, verbose_name=_('Date last access'))
- access_by = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Access by'))
+ date_last_login = models.DateTimeField(null=True, blank=True, verbose_name=_('Date last access'))
+ login_by = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Access by'))
date_change_secret = models.DateTimeField(null=True, blank=True, verbose_name=_('Date change secret'))
change_secret_status = models.CharField(max_length=16, null=True, blank=True, verbose_name=_('Change secret status'))
diff --git a/apps/accounts/models/automations/gather_account.py b/apps/accounts/models/automations/gather_account.py
index 6a40a0498..661ccfd46 100644
--- a/apps/accounts/models/automations/gather_account.py
+++ b/apps/accounts/models/automations/gather_account.py
@@ -1,9 +1,11 @@
from django.db import models
from django.db.models import Q
+from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from accounts.const import AutomationTypes, Source
from accounts.models import Account
+from common.const import ConfirmOrIgnore
from orgs.mixins.models import JMSOrgBaseModel
from .base import AccountBaseAutomation
@@ -11,19 +13,46 @@ __all__ = ['GatherAccountsAutomation', 'GatheredAccount']
class GatheredAccount(JMSOrgBaseModel):
- present = models.BooleanField(default=True, verbose_name=_("Present"))
+ present = models.BooleanField(default=True, verbose_name=_("Present")) # 资产上是否还存在
date_last_login = models.DateTimeField(null=True, verbose_name=_("Date login"))
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_("Asset"))
username = models.CharField(max_length=32, blank=True, db_index=True, verbose_name=_('Username'))
address_last_login = models.CharField(max_length=39, default='', verbose_name=_("Address login"))
+ status = models.CharField(max_length=32, default='', blank=True, choices=ConfirmOrIgnore.choices, verbose_name=_("Action"))
@property
def address(self):
return self.asset.address
- @staticmethod
- def sync_accounts(gathered_accounts):
+ @classmethod
+ def update_exists_accounts(cls, gathered_account, accounts):
+ if not gathered_account.date_last_login:
+ return
+
+ for account in accounts:
+ if (not account.date_last_login or
+ account.date_last_login - gathered_account.date_last_login > timezone.timedelta(minutes=5)):
+ account.date_last_login = gathered_account.date_last_login
+ account.login_by = '{}({})'.format('unknown', gathered_account.address_last_login)
+ account.save(update_fields=['date_last_login', 'login_by'])
+
+ @classmethod
+ def create_accounts(cls, gathered_account, accounts):
account_objs = []
+ asset_id = gathered_account.asset_id
+ username = gathered_account.username
+ access_by = '{}({})'.format('unknown', gathered_account.address_last_login)
+ account = Account(
+ asset_id=asset_id, username=username,
+ name=username, source=Source.COLLECTED,
+ date_last_access=gathered_account.date_last_login,
+ access_by=access_by
+ )
+ account_objs.append(account)
+ Account.objects.bulk_create(account_objs)
+
+ @classmethod
+ def sync_accounts(cls, gathered_accounts):
for gathered_account in gathered_accounts:
asset_id = gathered_account.asset_id
username = gathered_account.username
@@ -32,13 +61,12 @@ class GatheredAccount(JMSOrgBaseModel):
Q(asset_id=asset_id, name=username)
)
if accounts.exists():
- continue
- account = Account(
- asset_id=asset_id, username=username,
- name=username, source=Source.COLLECTED
- )
- account_objs.append(account)
- Account.objects.bulk_create(account_objs)
+ cls.update_exists_accounts(gathered_account, accounts)
+ else:
+ cls.create_accounts(gathered_account, accounts)
+
+ gathered_account.status = ConfirmOrIgnore.confirmed
+ gathered_account.save(update_fields=['action'])
class Meta:
verbose_name = _("Gather asset accounts")
diff --git a/apps/accounts/serializers/account/account.py b/apps/accounts/serializers/account/account.py
index 27e3f8303..70982f64e 100644
--- a/apps/accounts/serializers/account/account.py
+++ b/apps/accounts/serializers/account/account.py
@@ -236,7 +236,7 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
class Meta(BaseAccountSerializer.Meta):
model = Account
automation_fields = [
- 'date_last_access', 'access_by', 'date_verified', 'connectivity',
+ 'date_last_login', 'login_by', 'date_verified', 'connectivity',
'date_change_secret', 'change_secret_status'
]
fields = BaseAccountSerializer.Meta.fields + [
diff --git a/apps/accounts/serializers/account/gathered_account.py b/apps/accounts/serializers/account/gathered_account.py
index a36bddc5e..a3d35d9bc 100644
--- a/apps/accounts/serializers/account/gathered_account.py
+++ b/apps/accounts/serializers/account/gathered_account.py
@@ -2,10 +2,15 @@ from django.utils.translation import gettext_lazy as _
from accounts.models import GatheredAccount
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
-from .account import AccountAssetSerializer
+from .account import AccountAssetSerializer as _AccountAssetSerializer
from .base import BaseAccountSerializer
+class AccountAssetSerializer(_AccountAssetSerializer):
+ class Meta(_AccountAssetSerializer.Meta):
+ fields = [f for f in _AccountAssetSerializer.Meta.fields if f != 'auto_config']
+
+
class GatheredAccountSerializer(BulkOrgResourceModelSerializer):
asset = AccountAssetSerializer(label=_('Asset'))
@@ -13,7 +18,8 @@ class GatheredAccountSerializer(BulkOrgResourceModelSerializer):
model = GatheredAccount
fields = [
'id', 'present', 'asset', 'username',
- 'date_updated', 'address_last_login', 'date_last_login'
+ 'date_updated', 'address_last_login',
+ 'date_last_login', 'status'
]
@classmethod
diff --git a/apps/accounts/tasks/gather_accounts.py b/apps/accounts/tasks/gather_accounts.py
index 831a9fdf6..a02055a63 100644
--- a/apps/accounts/tasks/gather_accounts.py
+++ b/apps/accounts/tasks/gather_accounts.py
@@ -1,39 +1,33 @@
# ~*~ coding: utf-8 ~*~
-from celery import shared_task
-from django.utils.translation import gettext_lazy as _
-from django.utils.translation import gettext_noop
-from accounts.const import AutomationTypes
-from accounts.tasks.common import quickstart_automation_by_snapshot
-from assets.models import Node
from common.utils import get_logger
-from orgs.utils import org_aware_func
-__all__ = ['gather_asset_accounts_task']
+# __all__ = ['gather_asset_accounts_task']
logger = get_logger(__name__)
-
-@org_aware_func("nodes")
-def gather_asset_accounts_util(nodes, task_name):
- from accounts.models import GatherAccountsAutomation
- task_name = GatherAccountsAutomation.generate_unique_name(task_name)
-
- task_snapshot = {
- 'nodes': [str(node.id) for node in nodes],
- }
- tp = AutomationTypes.verify_account
- quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
-
-
-@shared_task(
- queue="ansible",
- verbose_name=_('Gather asset accounts'),
- activity_callback=lambda self, node_ids, task_name=None, *args, **kwargs: (node_ids, None),
- description=_("Unused")
-)
-def gather_asset_accounts_task(node_ids, task_name=None):
- if task_name is None:
- task_name = gettext_noop("Gather assets accounts")
-
- nodes = Node.objects.filter(id__in=node_ids)
- gather_asset_accounts_util(nodes=nodes, task_name=task_name)
+#
+# @org_aware_func("nodes")
+# def gather_asset_accounts_util(nodes, task_name):
+# from accounts.models import GatherAccountsAutomation
+# task_name = GatherAccountsAutomation.generate_unique_name(task_name)
+#
+# task_snapshot = {
+# 'nodes': [str(node.id) for node in nodes],
+# }
+# tp = AutomationTypes.verify_account
+# quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
+#
+#
+# @shared_task(
+# queue="ansible",
+# verbose_name=_('Gather asset accounts'),
+# activity_callback=lambda self, node_ids, task_name=None, *args, **kwargs: (node_ids, None),
+# description=_("Unused")
+# )
+# def gather_asset_accounts_task(node_ids, task_name=None):
+# if task_name is None:
+# task_name = gettext_noop("Gather assets accounts")
+#
+# nodes = Node.objects.filter(id__in=node_ids)
+# gather_asset_accounts_util(nodes=nodes, task_name=task_name)
+#
diff --git a/apps/assets/models/automations/base.py b/apps/assets/models/automations/base.py
index da0f73a1e..660552b8f 100644
--- a/apps/assets/models/automations/base.py
+++ b/apps/assets/models/automations/base.py
@@ -131,8 +131,8 @@ class AutomationExecution(OrgModelMixin):
return self.snapshot['type']
def get_all_asset_ids(self):
- node_ids = self.snapshot['nodes']
- asset_ids = self.snapshot['assets']
+ node_ids = self.snapshot.get('nodes', [])
+ asset_ids = self.snapshot.get('assets', [])
nodes = Node.objects.filter(id__in=node_ids)
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
asset_ids = set(list(asset_ids) + list(node_asset_ids))
diff --git a/apps/common/const/choices.py b/apps/common/const/choices.py
index 3eb0f2a58..935c68992 100644
--- a/apps/common/const/choices.py
+++ b/apps/common/const/choices.py
@@ -75,4 +75,9 @@ class Language(models.TextChoices):
jp = 'ja', '日本語',
+class ConfirmOrIgnore(models.TextChoices):
+ confirmed = 'confirmed', _('Confirmed')
+ ignored = 'ignored', _('Ignored')
+
+
COUNTRY_CALLING_CODES = get_country_phone_choices()
diff --git a/apps/templates/_head_css_js.html b/apps/templates/_head_css_js.html
index c9ff646e4..b6ed25e61 100644
--- a/apps/templates/_head_css_js.html
+++ b/apps/templates/_head_css_js.html
@@ -3,6 +3,7 @@
+