Skip to content

Refresh inventory detection and extend engine coverage#39

Merged
jruszo merged 2 commits intomasterfrom
feature/inventory-refresh
May 4, 2026
Merged

Refresh inventory detection and extend engine coverage#39
jruszo merged 2 commits intomasterfrom
feature/inventory-refresh

Conversation

@jruszo
Copy link
Copy Markdown
Owner

@jruszo jruszo commented May 4, 2026

Summary

  • Refresh instance inventory payloads and expose detected hostnames in the API and serializers
  • Update the inventory list UI and data-table behavior to handle the refreshed data shape
  • Expand SQL engine inventory support across ClickHouse, MSSQL, MySQL, Oracle, and PostgreSQL
  • Add migration and tests covering legacy behavior, inventory refresh logic, and engine-specific detection

Testing

  • Added and updated backend tests for inventory refresh and engine detection
  • Added frontend coverage for the feature registry and inventory list rendering
  • Migration drift and broader validation were included in the existing test suite updates

Summary by CodeRabbit

  • New Features

    • New configurable "Inventory refresh interval" in system settings and background scheduling to keep instance host/version info up to date
    • Instance list shows inventory status, detected hostname/version, and last refresh time with sortable columns, status badges, and row styling
    • Connection test now triggers an inventory refresh and persists detected metadata
  • Bug Fixes

    • Improved engine fallbacks to avoid errors when fetching instance hostname/version

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

📝 Walkthrough

Walkthrough

Adds a database-backed inventory refresh system: model fields and migration, engine-specific inventory detection, scheduler/task wiring, API/settings integration, serializers/views, frontend display and settings, and tests covering behavior and engine fallbacks.

Changes

Inventory Refresh System

Layer / File(s) Summary
Data Model & Migration
sql/models.py, sql/migrations/0016_instance_inventory_detected_hostname_and_more.py
Adds instance inventory fields: inventory_status, inventory_last_attempt_at, inventory_last_success_at, inventory_detected_hostname, inventory_detected_version and migration to persist them.
Domain Constants & Utilities
sql/inventory.py
Defines interval constants/choices/deltas, interval parsing/normalization helpers, and next-run calculation.
Scheduling Core
sql/inventory.py
Implements singleton one-shot schedule creation (schedule_inventory_refresh), lock via Config + select_for_update, and ensure_inventory_refresh_schedule(force=False).
Refresh Execution & Aggregation
sql/inventory.py
Adds collect_inventory_snapshot, refresh_instance_inventory_snapshot, refresh_inventory_snapshots, and inventory_refresh_task_callback which re-arms schedule.
Engine Inventory Plumbing
sql/engines/__init__.py, .../clickhouse.py, .../mssql.py, .../mysql.py, .../oracle.py, .../pgsql.py
Adds EngineBase.get_inventory_details() and per-engine get_inventory_details() implementations with guarded queries and fallbacks; ClickHouse server_version made robust to errors.
API Settings & Sync Helper
api_admin/settings.py
Adds inventory_refresh_interval schema/option, exposes inventory_refresh_intervals, adds sync_inventory_refresh_schedule(force=False) helper; SystemSettingsView.get() calls sync (non-forced) and put() forces sync and returns inventory_refresh_schedule_synced plus conditional detail.
API Instance Serialization & Views
api_instances/serializers.py, api_instances/views.py
InstanceListSerializer exposes inventory fields; InstanceList.get_queryset() adds search/ordering keys; .get() ensures schedule exists (logs on failure); connection test route uses refresh_instance_inventory_snapshot() and treats snapshot success boolean as the result.
Frontend Types & Settings UI
frontend/src/lib/api.ts, frontend/src/features/settings/system-settings.ts
Adds inventory_refresh_intervals to SystemSettingsOptions and inventory_* fields to InstanceInventoryRecord; adds inventory_refresh_interval select to settings form (default 24h).
Frontend Inventory List UI & Table Styling
frontend/src/components/ui/data-table/DataTable.vue, frontend/src/features/inventory/pages/InventoryListPage.vue
DataTable gets optional rowClass prop; InventoryList adds sortable columns for inventory fields, remaps inventory_last_refresh_atinventory_last_success_at for ordering, renders status badges, formats timestamps, and styles rows by status.
Tests: Engines, Inventory, API, Frontend
sql/engines/*_tests.py, sql/engines/tests.py, common/tests.py, api_core/legacy_tests.py, frontend/src/app/feature-registry.test.ts
Unit tests added for per-engine get_inventory_details() fallbacks and cursor cleanup; InventoryRefreshTests covering scheduling, re-arming, snapshot status transitions and formatting; API tests assert settings persistence and inventory_refresh_schedule_synced behavior; frontend test expectations updated.

Sequence Diagram

sequenceDiagram
    participant User
    participant Frontend
    participant SettingsAPI
    participant Scheduler
    participant RefreshTask
    participant Engine
    participant DB

    User->>Frontend: Open settings or inventory list
    Frontend->>SettingsAPI: GET /system-settings or GET /instances
    SettingsAPI->>Scheduler: ensure_inventory_refresh_schedule()
    Scheduler->>DB: create/reuse one-shot TaskSchedule (locked via Config)
    SettingsAPI-->>Frontend: return settings/options or instances (with inventory fields)

    User->>Frontend: Update inventory interval + Save
    Frontend->>SettingsAPI: PUT /system-settings {inventory_refresh_interval}
    SettingsAPI->>Scheduler: sync_inventory_refresh_schedule(force=True)
    Scheduler->>DB: reschedule one-shot TaskSchedule
    SettingsAPI-->>Frontend: {inventory_refresh_schedule_synced: true/false}

    Note over Scheduler,RefreshTask: At scheduled time
    Scheduler->>RefreshTask: fire refresh task
    RefreshTask->>DB: list Instances (ordered)
    loop per Instance
      RefreshTask->>Engine: connect & get_inventory_details()
      Engine->>DB: query server properties (hostname/version)
      Engine-->>RefreshTask: {hostname, version}
      RefreshTask->>DB: update Instance inventory_{status,last_* ,detected_*}
    end
    RefreshTask->>Scheduler: inventory_refresh_task_callback()
    Scheduler->>DB: re-arm next one-shot schedule
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰 I hopped through schedules, clocks ticking slow,

I asked each engine what names they know,
I stored tiny whispers of version and host,
I guard the one-shot so none run the most,
Now badges and rows show the snapshots we chose. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main changes: expanding inventory detection capabilities and adding engine-specific implementations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/inventory-refresh

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
api_core/legacy_tests.py (1)

2319-2345: ⚠️ Potential issue | 🟠 Major

Rename one of the duplicate TestPermissionRequestAPI classes; this updated test is currently shadowed.

api_core/legacy_tests.py defines TestPermissionRequestAPI twice (lines 2085 and 4511). In Python, the later class definition overwrites the earlier binding in the module namespace, so tests in the first class—including this updated test at lines 2319-2345—are not discovered by unittest.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api_core/legacy_tests.py` around lines 2319 - 2345, The module defines
TestPermissionRequestAPI twice which causes the first definition (containing the
updated test at test_test_instance_connection) to be shadowed; locate both class
definitions named TestPermissionRequestAPI and rename one to a unique name
(e.g., TestPermissionRequestAPI_Legacy or TestPermissionRequestAPI_Old) so the
earlier test class is preserved and discovered by unittest; ensure the chosen
new class name is updated wherever referenced in the file (if any) and that
there are no duplicate test class names remaining.
🧹 Nitpick comments (1)
sql/engines/clickhouse.py (1)

58-68: 💤 Low value

server_version: raising ValueError inside the same try block that catches bare Exception (BLE001).

The pattern raises a ValueError as a control-flow signal and then catches it alongside all other exceptions. Restructuring separates query failure (already handled by result.error) from parsing failure and avoids the broad catch that static analysis flags.

♻️ Suggested refactor
 `@property`
 def server_version(self):
     sql = "select value from system.build_options where name = 'VERSION_FULL';"
-    try:
-        result = self.query(sql=sql)
-        if result.error or not result.rows or not result.rows[0]:
-            raise ValueError(result.error or "empty result")
-        version = result.rows[0][0].split(" ")[1]
-        return tuple([int(n) for n in version.split(".")[:3]])
-    except Exception as exc:
-        logger.warning("Failed to fetch ClickHouse server version: %s", exc)
-        return tuple()
+    result = self.query(sql=sql)
+    if result.error or not result.rows or not result.rows[0]:
+        logger.warning("Failed to fetch ClickHouse server version: %s", result.error or "empty result")
+        return tuple()
+    try:
+        version = result.rows[0][0].split(" ")[1]
+        return tuple(int(n) for n in version.split(".")[:3])
+    except (IndexError, ValueError) as exc:
+        logger.warning("Failed to parse ClickHouse server version: %s", exc)
+        return tuple()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sql/engines/clickhouse.py` around lines 58 - 68, In server_version, avoid
raising ValueError inside the broad try that immediately catches Exception;
instead check result.error and empty results immediately after calling
self.query and return/handle there, then move only the parsing logic into a
narrow try that catches specific parsing errors (e.g., ValueError/IndexError) or
let them propagate; reference the server_version method and the result variable
returned by self.query to locate where to separate query-failure handling from
version-string parsing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api_instances/views.py`:
- Around line 513-514: The API docs listing searchable fields (currently showing
only ID/name/host/user) are out of sync with the view that now performs
Q(inventory_detected_hostname__icontains=search) and
Q(inventory_detected_version__icontains=search); update the
schema/docstring/OpenAPI spec entry that enumerates searchable fields to include
"inventory_detected_hostname" and "inventory_detected_version" (and any matching
human-friendly labels), and update any examples/tests that assert the old list
so docs and tests match the actual search behavior implemented in the view.

In `@common/tests.py`:
- Around line 150-157: The test assumes a default interval longer than "1h"
which may change; make it deterministic by explicitly setting a known initial
interval before the first schedule creation: call
self.sys_config.set("inventory_refresh_interval", "<initial_interval>") (e.g.
"2h"), call inventory.ensure_inventory_refresh_schedule() to obtain first_run
(via
TaskSchedule.objects.get(name=inventory.INVENTORY_REFRESH_SCHEDULE_NAME).run_at),
then change the interval to "1h", call
inventory.ensure_inventory_refresh_schedule(force=True) to obtain second_run,
and finally assert second_run < first_run and mock_schedule.call_count == 2.

In `@frontend/src/features/inventory/pages/InventoryListPage.vue`:
- Line 374: The search placeholder in InventoryListPage.vue is out of sync with
the :search-keys array (which now includes 'inventory_detected_hostname' and
'inventory_detected_version'); update the placeholder text used by the search
input (the component/prop that renders the search box in
InventoryListPage.vue—e.g., the placeholder or search-placeholder prop) to
mention detected hostname and detected version (for example: "Search by name,
host, user, ID, detected hostname or detected version") so the UI matches the
searchable fields.

In `@sql/engines/mysql.py`:
- Around line 158-171: get_inventory_details currently uses a raw cursor
(cursor.execute/cursor.fetchone) so DB errors can propagate; change it to follow
PgSQL/MSSQL behavior by using self.query("SELECT @@hostname") or wrapping the
cursor calls in try/except that catches exceptions, logs a warning, closes the
cursor in finally, and returns default_details on failure. Also protect the
conn.get_server_info() call similarly (return default_details if it raises) so
any exception from hostname lookup or server_info does not bubble up; keep
references to get_inventory_details, self.query, cursor.execute,
cursor.fetchone, conn.get_server_info, and default_details when implementing the
fix.

In `@sql/engines/test_mssql.py`:
- Around line 91-96: The test
test_get_inventory_details_returns_fallback_when_query_fails is not actually
simulating a query error because ResultSet.__init__ ignores an error kwarg;
update the test to construct the ResultSet normally then set its error attribute
explicitly (e.g. rs = ResultSet(rows=[]); rs.error = "boom";
mock_query.return_value = rs) so MssqlEngine.get_inventory_details sees
result.error truthy and exercises the error branch rather than only the
empty-rows path.

---

Outside diff comments:
In `@api_core/legacy_tests.py`:
- Around line 2319-2345: The module defines TestPermissionRequestAPI twice which
causes the first definition (containing the updated test at
test_test_instance_connection) to be shadowed; locate both class definitions
named TestPermissionRequestAPI and rename one to a unique name (e.g.,
TestPermissionRequestAPI_Legacy or TestPermissionRequestAPI_Old) so the earlier
test class is preserved and discovered by unittest; ensure the chosen new class
name is updated wherever referenced in the file (if any) and that there are no
duplicate test class names remaining.

---

Nitpick comments:
In `@sql/engines/clickhouse.py`:
- Around line 58-68: In server_version, avoid raising ValueError inside the
broad try that immediately catches Exception; instead check result.error and
empty results immediately after calling self.query and return/handle there, then
move only the parsing logic into a narrow try that catches specific parsing
errors (e.g., ValueError/IndexError) or let them propagate; reference the
server_version method and the result variable returned by self.query to locate
where to separate query-failure handling from version-string parsing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8a7c02ba-fcf7-4a36-9b94-903cd5cdc175

📥 Commits

Reviewing files that changed from the base of the PR and between 9ed6dd5 and 3d60379.

📒 Files selected for processing (22)
  • api_admin/settings.py
  • api_core/legacy_tests.py
  • api_instances/serializers.py
  • api_instances/views.py
  • common/tests.py
  • frontend/src/app/feature-registry.test.ts
  • frontend/src/components/ui/data-table/DataTable.vue
  • frontend/src/features/inventory/pages/InventoryListPage.vue
  • frontend/src/features/settings/system-settings.ts
  • frontend/src/lib/api.ts
  • sql/engines/__init__.py
  • sql/engines/clickhouse.py
  • sql/engines/mssql.py
  • sql/engines/mysql.py
  • sql/engines/oracle.py
  • sql/engines/pgsql.py
  • sql/engines/test_mssql.py
  • sql/engines/test_mysql.py
  • sql/engines/tests.py
  • sql/inventory.py
  • sql/migrations/0016_instance_inventory_detected_hostname_and_more.py
  • sql/models.py

Comment thread api_instances/views.py
Comment thread common/tests.py
Comment thread frontend/src/features/inventory/pages/InventoryListPage.vue
Comment thread sql/engines/mysql.py Outdated
Comment thread sql/engines/test_mssql.py
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
api_core/legacy_tests.py (1)

1536-1536: ⚡ Quick win

Use timezone.now() instead of datetime.now() for the DateTimeField.

DateTimeField with USE_TZ = True expects only aware datetimes; passing a naive one from datetime.now() raises a RuntimeWarning. django.utils.timezone.now() is the idiomatic replacement — it returns an aware datetime when USE_TZ = True and a naive one when False, making it safe in all configurations.

🔧 Proposed fix
+from django.utils import timezone
 ...
-        self.ins.inventory_last_success_at = datetime.now()
+        self.ins.inventory_last_success_at = timezone.now()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api_core/legacy_tests.py` at line 1536, Replace the naive datetime.now() with
Django's timezone-aware helper when setting the DateTimeField: change the
assignment to use django.utils.timezone.now() for
self.ins.inventory_last_success_at (and add an import for timezone at top if
missing) so the value is timezone-aware when USE_TZ=True and avoids
RuntimeWarning.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@api_core/legacy_tests.py`:
- Line 1536: Replace the naive datetime.now() with Django's timezone-aware
helper when setting the DateTimeField: change the assignment to use
django.utils.timezone.now() for self.ins.inventory_last_success_at (and add an
import for timezone at top if missing) so the value is timezone-aware when
USE_TZ=True and avoids RuntimeWarning.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9b41f4e4-51c7-4b9a-9362-6d66578235ae

📥 Commits

Reviewing files that changed from the base of the PR and between 3d60379 and 30bd115.

📒 Files selected for processing (8)
  • api_core/legacy_tests.py
  • api_instances/views.py
  • common/tests.py
  • frontend/src/features/inventory/pages/InventoryListPage.vue
  • sql/engines/clickhouse.py
  • sql/engines/mysql.py
  • sql/engines/test_mssql.py
  • sql/engines/test_mysql.py
✅ Files skipped from review due to trivial changes (1)
  • sql/engines/test_mssql.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • sql/engines/test_mysql.py
  • frontend/src/features/inventory/pages/InventoryListPage.vue

@jruszo jruszo merged commit f68302b into master May 4, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant