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