фикс политик
This commit is contained in:
parent
1ff64e4881
commit
735d8b2736
6 changed files with 52 additions and 44 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Revision ID: 0020
|
Revision ID: 0020
|
||||||
Revises: 0019
|
Revises: 0019
|
||||||
Create Date: 2026-05-27
|
Create Date: 2026-05-28
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
@ -17,10 +17,11 @@ depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
|
op.execute("DROP TABLE IF EXISTS snapshot_policies")
|
||||||
op.create_table(
|
op.create_table(
|
||||||
"snapshot_policies",
|
"snapshot_policies",
|
||||||
sa.Column("id", sa.Integer(), autoincrement=True, primary_key=True),
|
sa.Column("id", sa.Integer(), autoincrement=True, primary_key=True),
|
||||||
sa.Column("category", sa.String(50), nullable=False),
|
sa.Column("categories", postgresql.ARRAY(sa.String(50)), nullable=False),
|
||||||
sa.Column("action_type", sa.String(100), nullable=False),
|
sa.Column("action_type", sa.String(100), nullable=False),
|
||||||
sa.Column("params", postgresql.JSONB(), nullable=False, server_default="{}"),
|
sa.Column("params", postgresql.JSONB(), nullable=False, server_default="{}"),
|
||||||
sa.Column("label", sa.String(255), nullable=False),
|
sa.Column("label", sa.String(255), nullable=False),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from sqlalchemy import Boolean, Integer, String
|
from sqlalchemy import Boolean, Integer, String
|
||||||
from sqlalchemy.dialects.postgresql import JSONB
|
from sqlalchemy.dialects.postgresql import ARRAY, JSONB
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from .base import Base, TimestampMixin
|
from .base import Base, TimestampMixin
|
||||||
|
|
@ -9,7 +9,7 @@ class SnapshotPolicy(Base, TimestampMixin):
|
||||||
__tablename__ = "snapshot_policies"
|
__tablename__ = "snapshot_policies"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(Integer(), autoincrement=True, primary_key=True)
|
id: Mapped[int] = mapped_column(Integer(), autoincrement=True, primary_key=True)
|
||||||
category: Mapped[str] = mapped_column(String(50), nullable=False)
|
categories: Mapped[list[str]] = mapped_column(ARRAY(String(50)), nullable=False)
|
||||||
action_type: Mapped[str] = mapped_column(String(100), nullable=False)
|
action_type: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||||
params: Mapped[dict] = mapped_column(JSONB(), nullable=False, server_default="{}")
|
params: Mapped[dict] = mapped_column(JSONB(), nullable=False, server_default="{}")
|
||||||
label: Mapped[str] = mapped_column(String(255), nullable=False)
|
label: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,10 @@ async def snapshot_dashboard(
|
||||||
if not policies:
|
if not policies:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Collect categories we care about
|
# Collect all categories across all policies
|
||||||
categories = list({p.category for p in policies})
|
all_categories = list({cat for p in policies for cat in p.categories})
|
||||||
|
|
||||||
# 2. Find all active objects that have devices in relevant categories
|
# 2. Find all active objects
|
||||||
obj_q = (
|
obj_q = (
|
||||||
select(Object.id, Object.name, Object.city, Object.client)
|
select(Object.id, Object.name, Object.city, Object.client)
|
||||||
.where(Object.is_active == True)
|
.where(Object.is_active == True)
|
||||||
|
|
@ -65,7 +65,7 @@ async def snapshot_dashboard(
|
||||||
Device.category,
|
Device.category,
|
||||||
func.count(Device.id).label("total"),
|
func.count(Device.id).label("total"),
|
||||||
)
|
)
|
||||||
.where(Device.is_active == True, Device.category.in_(categories))
|
.where(Device.is_active == True, Device.category.in_(all_categories))
|
||||||
.group_by(Device.object_id, Device.category)
|
.group_by(Device.object_id, Device.category)
|
||||||
)
|
)
|
||||||
eligible_result = await db.execute(eligible_q)
|
eligible_result = await db.execute(eligible_q)
|
||||||
|
|
@ -83,7 +83,7 @@ async def snapshot_dashboard(
|
||||||
func.max(ConfigSnapshot.created_at).label("last_collected_at"),
|
func.max(ConfigSnapshot.created_at).label("last_collected_at"),
|
||||||
)
|
)
|
||||||
.join(Device, ConfigSnapshot.device_id == Device.id)
|
.join(Device, ConfigSnapshot.device_id == Device.id)
|
||||||
.where(Device.is_active == True, Device.category.in_(categories))
|
.where(Device.is_active == True, Device.category.in_(all_categories))
|
||||||
.group_by(Device.object_id, Device.category, ConfigSnapshot.label)
|
.group_by(Device.object_id, Device.category, ConfigSnapshot.label)
|
||||||
)
|
)
|
||||||
stats_result = await db.execute(stats_q)
|
stats_result = await db.execute(stats_q)
|
||||||
|
|
@ -97,18 +97,27 @@ async def snapshot_dashboard(
|
||||||
for obj_id, obj_name, obj_city, obj_client in all_objects:
|
for obj_id, obj_name, obj_city, obj_client in all_objects:
|
||||||
obj_policies = []
|
obj_policies = []
|
||||||
for policy in policies:
|
for policy in policies:
|
||||||
total = eligible_map.get((obj_id, policy.category), 0)
|
# Sum across all categories in this policy
|
||||||
|
total = sum(eligible_map.get((obj_id, cat), 0) for cat in policy.categories)
|
||||||
if total == 0:
|
if total == 0:
|
||||||
continue
|
continue
|
||||||
snap_label = _policy_snapshot_label(policy)
|
snap_label = _policy_snapshot_label(policy)
|
||||||
stats = stats_map.get((obj_id, policy.category, snap_label))
|
success_count = sum(
|
||||||
success_count = stats[0] if stats else 0
|
(stats_map.get((obj_id, cat, snap_label)) or (0, None))[0]
|
||||||
last_collected_at = stats[1] if stats else None
|
for cat in policy.categories
|
||||||
|
)
|
||||||
|
# Latest collected_at across categories
|
||||||
|
last_collected_at = None
|
||||||
|
for cat in policy.categories:
|
||||||
|
stats = stats_map.get((obj_id, cat, snap_label))
|
||||||
|
if stats and stats[1]:
|
||||||
|
if last_collected_at is None or stats[1] > last_collected_at:
|
||||||
|
last_collected_at = stats[1]
|
||||||
|
|
||||||
obj_policies.append(PolicySnapshotStatus(
|
obj_policies.append(PolicySnapshotStatus(
|
||||||
policy_id=policy.id,
|
policy_id=policy.id,
|
||||||
policy_label=policy.label,
|
policy_label=policy.label,
|
||||||
category=policy.category,
|
categories=policy.categories,
|
||||||
action_type=policy.action_type,
|
action_type=policy.action_type,
|
||||||
params=policy.params,
|
params=policy.params,
|
||||||
last_collected_at=last_collected_at,
|
last_collected_at=last_collected_at,
|
||||||
|
|
@ -148,7 +157,7 @@ async def create_policy(
|
||||||
_: User = Depends(get_current_user),
|
_: User = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
policy = SnapshotPolicy(
|
policy = SnapshotPolicy(
|
||||||
category=body.category,
|
categories=body.categories,
|
||||||
action_type=body.action_type,
|
action_type=body.action_type,
|
||||||
params=body.params,
|
params=body.params,
|
||||||
label=body.label,
|
label=body.label,
|
||||||
|
|
@ -201,7 +210,7 @@ async def collect_all(
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
"""Collect snapshots for ALL objects matching this policy's category."""
|
"""Collect snapshots for ALL objects matching this policy's categories."""
|
||||||
policy = await db.get(SnapshotPolicy, policy_id)
|
policy = await db.get(SnapshotPolicy, policy_id)
|
||||||
if policy is None:
|
if policy is None:
|
||||||
raise HTTPException(status_code=404, detail="Policy not found")
|
raise HTTPException(status_code=404, detail="Policy not found")
|
||||||
|
|
@ -211,8 +220,8 @@ async def collect_all(
|
||||||
task = await task_service.create_task(
|
task = await task_service.create_task(
|
||||||
action_type=policy.action_type,
|
action_type=policy.action_type,
|
||||||
target_scope={
|
target_scope={
|
||||||
"type": "category",
|
"type": "filter",
|
||||||
"category": policy.category,
|
"category": policy.categories,
|
||||||
},
|
},
|
||||||
params=policy.params,
|
params=policy.params,
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
|
|
@ -227,7 +236,7 @@ async def collect_for_object(
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
"""Collect snapshots for a specific object matching this policy's category."""
|
"""Collect snapshots for a specific object matching this policy's categories."""
|
||||||
policy = await db.get(SnapshotPolicy, policy_id)
|
policy = await db.get(SnapshotPolicy, policy_id)
|
||||||
if policy is None:
|
if policy is None:
|
||||||
raise HTTPException(status_code=404, detail="Policy not found")
|
raise HTTPException(status_code=404, detail="Policy not found")
|
||||||
|
|
@ -241,9 +250,9 @@ async def collect_for_object(
|
||||||
task = await task_service.create_task(
|
task = await task_service.create_task(
|
||||||
action_type=policy.action_type,
|
action_type=policy.action_type,
|
||||||
target_scope={
|
target_scope={
|
||||||
"type": "category",
|
"type": "filter",
|
||||||
"object_id": obj_id,
|
"object_id": obj_id,
|
||||||
"category": policy.category,
|
"category": policy.categories,
|
||||||
},
|
},
|
||||||
params=policy.params,
|
params=policy.params,
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
|
|
@ -267,12 +276,12 @@ async def dashboard_devices(
|
||||||
|
|
||||||
snap_label = _policy_snapshot_label(policy)
|
snap_label = _policy_snapshot_label(policy)
|
||||||
|
|
||||||
# All eligible devices for this object+category
|
# All eligible devices for this object matching policy categories
|
||||||
devices_q = (
|
devices_q = (
|
||||||
select(Device.id, Device.ip, Device.hostname, Device.status)
|
select(Device.id, Device.ip, Device.hostname, Device.status)
|
||||||
.where(
|
.where(
|
||||||
Device.object_id == obj_id,
|
Device.object_id == obj_id,
|
||||||
Device.category == policy.category,
|
Device.category.in_(policy.categories),
|
||||||
Device.is_active == True,
|
Device.is_active == True,
|
||||||
)
|
)
|
||||||
.order_by(Device.ip)
|
.order_by(Device.ip)
|
||||||
|
|
@ -310,7 +319,6 @@ async def dashboard_devices(
|
||||||
.order_by(ConfigSnapshot.created_at.desc())
|
.order_by(ConfigSnapshot.created_at.desc())
|
||||||
)
|
)
|
||||||
snap_ids_result = await db.execute(snap_ids_q)
|
snap_ids_result = await db.execute(snap_ids_q)
|
||||||
# Take first (latest) per device
|
|
||||||
device_snap_map: dict[int, tuple[int, str]] = {}
|
device_snap_map: dict[int, tuple[int, str]] = {}
|
||||||
for r in snap_ids_result.all():
|
for r in snap_ids_result.all():
|
||||||
if r.device_id not in device_snap_map:
|
if r.device_id not in device_snap_map:
|
||||||
|
|
@ -349,4 +357,3 @@ async def get_snapshot_content(
|
||||||
raise HTTPException(status_code=404, detail="Content not found")
|
raise HTTPException(status_code=404, detail="Content not found")
|
||||||
|
|
||||||
return {"content": content_row.content}
|
return {"content": content_row.content}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
|
||||||
class SnapshotPolicyCreate(BaseModel):
|
class SnapshotPolicyCreate(BaseModel):
|
||||||
category: str
|
categories: list[str]
|
||||||
action_type: str
|
action_type: str
|
||||||
params: dict = {}
|
params: dict = {}
|
||||||
label: str
|
label: str
|
||||||
|
|
@ -13,7 +13,7 @@ class SnapshotPolicyCreate(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class SnapshotPolicyUpdate(BaseModel):
|
class SnapshotPolicyUpdate(BaseModel):
|
||||||
category: Optional[str] = None
|
categories: Optional[list[str]] = None
|
||||||
action_type: Optional[str] = None
|
action_type: Optional[str] = None
|
||||||
params: Optional[dict] = None
|
params: Optional[dict] = None
|
||||||
label: Optional[str] = None
|
label: Optional[str] = None
|
||||||
|
|
@ -24,7 +24,7 @@ class SnapshotPolicyRead(BaseModel):
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
id: int
|
id: int
|
||||||
category: str
|
categories: list[str]
|
||||||
action_type: str
|
action_type: str
|
||||||
params: dict
|
params: dict
|
||||||
label: str
|
label: str
|
||||||
|
|
@ -36,7 +36,7 @@ class SnapshotPolicyRead(BaseModel):
|
||||||
class PolicySnapshotStatus(BaseModel):
|
class PolicySnapshotStatus(BaseModel):
|
||||||
policy_id: int
|
policy_id: int
|
||||||
policy_label: str
|
policy_label: str
|
||||||
category: str
|
categories: list[str]
|
||||||
action_type: str
|
action_type: str
|
||||||
params: dict
|
params: dict
|
||||||
last_collected_at: Optional[datetime] = None
|
last_collected_at: Optional[datetime] = None
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { api } from './client'
|
||||||
|
|
||||||
export interface SnapshotPolicy {
|
export interface SnapshotPolicy {
|
||||||
id: number
|
id: number
|
||||||
category: string
|
categories: string[]
|
||||||
action_type: string
|
action_type: string
|
||||||
params: Record<string, unknown>
|
params: Record<string, unknown>
|
||||||
label: string
|
label: string
|
||||||
|
|
@ -15,7 +15,7 @@ export interface SnapshotPolicy {
|
||||||
export interface PolicySnapshotStatus {
|
export interface PolicySnapshotStatus {
|
||||||
policy_id: number
|
policy_id: number
|
||||||
policy_label: string
|
policy_label: string
|
||||||
category: string
|
categories: string[]
|
||||||
action_type: string
|
action_type: string
|
||||||
params: Record<string, unknown>
|
params: Record<string, unknown>
|
||||||
last_collected_at: string | null
|
last_collected_at: string | null
|
||||||
|
|
@ -53,7 +53,7 @@ export const useCreateSnapshotPolicy = () => {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (body: {
|
mutationFn: (body: {
|
||||||
category: string
|
categories: string[]
|
||||||
action_type: string
|
action_type: string
|
||||||
params?: Record<string, unknown>
|
params?: Record<string, unknown>
|
||||||
label: string
|
label: string
|
||||||
|
|
@ -77,7 +77,7 @@ export const useUpdateSnapshotPolicy = () => {
|
||||||
...body
|
...body
|
||||||
}: {
|
}: {
|
||||||
policyId: number
|
policyId: number
|
||||||
category?: string
|
categories?: string[]
|
||||||
action_type?: string
|
action_type?: string
|
||||||
params?: Record<string, unknown>
|
params?: Record<string, unknown>
|
||||||
label?: string
|
label?: string
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ const ACTION_OPTIONS = [
|
||||||
|
|
||||||
interface PolicyFormValues {
|
interface PolicyFormValues {
|
||||||
label: string
|
label: string
|
||||||
category: string
|
categories: string[]
|
||||||
action_type: string
|
action_type: string
|
||||||
path?: string
|
path?: string
|
||||||
}
|
}
|
||||||
|
|
@ -141,7 +141,7 @@ export function SnapshotDashboardPage() {
|
||||||
form.resetFields()
|
form.resetFields()
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
label: policy.label,
|
label: policy.label,
|
||||||
category: policy.category,
|
categories: policy.categories,
|
||||||
action_type: policy.action_type,
|
action_type: policy.action_type,
|
||||||
path: policy.action_type === 'get_file' ? (policy.params as any)?.path ?? '' : undefined,
|
path: policy.action_type === 'get_file' ? (policy.params as any)?.path ?? '' : undefined,
|
||||||
})
|
})
|
||||||
|
|
@ -161,7 +161,7 @@ export function SnapshotDashboardPage() {
|
||||||
await updateMutation.mutateAsync({
|
await updateMutation.mutateAsync({
|
||||||
policyId: editPolicy.id,
|
policyId: editPolicy.id,
|
||||||
label: values.label,
|
label: values.label,
|
||||||
category: values.category,
|
categories: values.categories,
|
||||||
action_type: values.action_type,
|
action_type: values.action_type,
|
||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
|
|
@ -169,7 +169,7 @@ export function SnapshotDashboardPage() {
|
||||||
} else {
|
} else {
|
||||||
await createMutation.mutateAsync({
|
await createMutation.mutateAsync({
|
||||||
label: values.label,
|
label: values.label,
|
||||||
category: values.category,
|
categories: values.categories,
|
||||||
action_type: values.action_type,
|
action_type: values.action_type,
|
||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
|
|
@ -212,10 +212,10 @@ export function SnapshotDashboardPage() {
|
||||||
const policyColumns: ColumnsType<SnapshotPolicy> = [
|
const policyColumns: ColumnsType<SnapshotPolicy> = [
|
||||||
{ title: 'Название', dataIndex: 'label', key: 'label' },
|
{ title: 'Название', dataIndex: 'label', key: 'label' },
|
||||||
{
|
{
|
||||||
title: 'Категория',
|
title: 'Категории',
|
||||||
dataIndex: 'category',
|
dataIndex: 'categories',
|
||||||
key: 'category',
|
key: 'categories',
|
||||||
render: (cat: string) => <Tag>{cat}</Tag>,
|
render: (cats: string[]) => cats.map((c) => <Tag key={c}>{c}</Tag>),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Действие',
|
title: 'Действие',
|
||||||
|
|
@ -329,8 +329,8 @@ export function SnapshotDashboardPage() {
|
||||||
<Form.Item name="label" label="Название" rules={[{ required: true, message: 'Введите название' }]}>
|
<Form.Item name="label" label="Название" rules={[{ required: true, message: 'Введите название' }]}>
|
||||||
<Input placeholder="CAPS config" />
|
<Input placeholder="CAPS config" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="category" label="Категория устройств" rules={[{ required: true }]}>
|
<Form.Item name="categories" label="Категории устройств" rules={[{ required: true }]}>
|
||||||
<Select options={CATEGORY_OPTIONS} placeholder="Выберите категорию" />
|
<Select mode="multiple" options={CATEGORY_OPTIONS} placeholder="Выберите категории" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="action_type" label="Действие" rules={[{ required: true }]}>
|
<Form.Item name="action_type" label="Действие" rules={[{ required: true }]}>
|
||||||
<Select options={ACTION_OPTIONS} placeholder="Выберите действие" />
|
<Select options={ACTION_OPTIONS} placeholder="Выберите действие" />
|
||||||
|
|
@ -480,7 +480,7 @@ function ObjectDevicesExpanded({
|
||||||
<div key={p.policy_id} style={{ marginBottom: 12 }}>
|
<div key={p.policy_id} style={{ marginBottom: 12 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
|
||||||
<Typography.Text strong>{p.policy_label}</Typography.Text>
|
<Typography.Text strong>{p.policy_label}</Typography.Text>
|
||||||
<Tag>{p.category}</Tag>
|
{p.categories.map((c) => <Tag key={c}>{c}</Tag>)}
|
||||||
<Tooltip title="Собрать">
|
<Tooltip title="Собрать">
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue