-
-
Notifications
You must be signed in to change notification settings - Fork 579
Add update checker management command and documentation #3127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
mlodic
merged 36 commits into
intelowlproject:develop
from
lvb05:feature/update-checker-2876
Feb 7, 2026
Merged
Changes from all commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
987765d
Merge pull request #2849 from intelowlproject/develop
drosetti 067ab3a
Add update checker management command, settings, and docs
lvb05 618f293
Format update checker with black and branch update
lvb05 eef5c15
Fix linting issues and improve update checker command output
lvb05 7c59a39
Fix ruff linting issues
lvb05 47ac868
Restore README.md (remove update checker documentation)
lvb05 0b12155
Fix formatting in update checker management command
lvb05 f07a054
Add persistent update check status model
lvb05 21dd0e2
Fix trailing whitespace in UpdateCheckStatus docstring
lvb05 14c37da
Use persistent state for update checks and add Celery task
lvb05 bfce6ca
Add admin GUI notification for update availability and formatting
lvb05 89eabf3
Add IntelOwl update check system with model, migration, scheduler and…
lvb05 a7f3f40
Merge branch 'develop' into feature/update-checker-2876
lvb05 95671b2
style: fix import ordering per ruff
lvb05 7b9fb40
fix: apply black formatting on tasks.py
lvb05 024d0a2
fix(update-checker): safe lazy imports and robust version handling
lvb05 ba4e056
fix(update-checker): prevent duplicate notifications and improve vers…
lvb05 0c9db74
Restore intel_owl/tasks.py to upstream version
lvb05 8839e99
Add automated update check system with celery task, model, command an…
lvb05 471dc63
fix: use __latest__ for django_celery_beat migration dependency
lvb05 c1681ba
Fix migration 0072: use literal 'days' instead of IntervalSchedule.DA…
lvb05 82398e2
Fix update-check notifications to work correctly with transactions an…
lvb05 385ef00
Fix update checker tests to use UserEventQuerySet notifications and i…
lvb05 7e17a87
Merge branch 'develop' into feature/update-checker-2876
lvb05 bff38e5
Apply Black formatting after rebase
lvb05 26f956b
Fix ruff formatting issue in commons settings
lvb05 c570770
Fix ruff formatting
lvb05 6f43f7e
.
lvb05 034ea13
.
lvb05 9bc6ee1
Add system update panel and API endpoint
lvb05 81a1e98
Fix lint issues and adjust tests
lvb05 09f9796
Add system update panel UI and fix lint/format issues
lvb05 eae37cf
Fix Prettier formatting for update checker components
lvb05 a554277
correct formatting and comments
lvb05 a8dec4f
system update notification show up on Home
lvb05 c1364d1
style change in notification
lvb05 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| from celery import shared_task | ||
|
|
||
| from api_app.core.update_checker import check_for_update | ||
|
|
||
|
|
||
| @shared_task | ||
| def scheduled_update_check(): | ||
| """ | ||
| Periodic task to check for IntelOwl updates. | ||
| Intended to be triggered via celery beat. | ||
| """ | ||
| check_for_update() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| import logging | ||
| from typing import Optional, Tuple | ||
|
|
||
| import requests | ||
| from django.conf import settings | ||
| from django.db import IntegrityError, transaction | ||
| from django.utils.timezone import now | ||
|
|
||
| from api_app.models import UpdateCheckStatus | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| try: | ||
| from api_app.user_events_manager.queryset import UserEventQuerySet | ||
| except Exception: | ||
| UserEventQuerySet = None | ||
|
|
||
| try: | ||
| from certego_saas_notifications.models import Notification | ||
| except Exception: | ||
| Notification = None | ||
|
|
||
| STORED_TAG_MAX_LEN = UpdateCheckStatus._meta.get_field("latest_version").max_length | ||
|
|
||
|
|
||
| def normalize_version(v: str) -> Tuple[int, ...]: | ||
| parts: list[int] = [] | ||
| for x in str(v).split("."): | ||
| if x.isdigit(): | ||
| parts.append(int(x)) | ||
| else: | ||
| break | ||
| return tuple(parts) | ||
|
|
||
|
|
||
| def fetch_latest_version() -> Tuple[Optional[str], Optional[str]]: | ||
| url = getattr(settings, "UPDATE_CHECK_URL", None) | ||
| if not url: | ||
| return None, "UPDATE_CHECK_URL not configured" | ||
|
|
||
| try: | ||
| resp = requests.get(url, headers={"User-Agent": "IntelOwl-Update-Checker"}, timeout=5) | ||
| except requests.RequestException as exc: | ||
| logger.error("Update check HTTP request failed: %s", exc) | ||
| return None, "Failed to fetch release information" | ||
|
|
||
| try: | ||
| resp.raise_for_status() | ||
| except requests.RequestException as exc: | ||
| logger.error("Update check HTTP error: %s", exc) | ||
| return None, "Update server returned an error" | ||
|
|
||
| try: | ||
| data = resp.json() | ||
| except ValueError: | ||
| logger.error("Invalid JSON from update server") | ||
| return None, "Invalid response from update server" | ||
|
|
||
| tag = data.get("tag_name") | ||
| if not tag: | ||
| logger.warning("Update response missing tag_name") | ||
| return None, "Release response missing tag_name" | ||
|
|
||
| return str(tag).lstrip("v"), None | ||
|
|
||
|
|
||
| def _notify_admins(title: str, message: str) -> None: | ||
| try: | ||
| if UserEventQuerySet is not None: | ||
| UserEventQuerySet.notify_admins(title=title, message=message, persistent=True, severity="warning") | ||
| return | ||
| if Notification is not None: | ||
| Notification.objects.create( | ||
| title=title, | ||
| description=message, | ||
| level="info", | ||
| for_admins=True, | ||
| ) | ||
| return | ||
| logger.info("No notification backend available; skipping admin notification") | ||
| except Exception: | ||
| logger.exception("Failed to send admin notification") | ||
|
|
||
|
|
||
| def check_for_update() -> Tuple[bool, str]: | ||
| current_version = getattr(settings, "INTEL_OWL_VERSION", None) | ||
| if not current_version: | ||
| return False, "INTEL_OWL_VERSION setting missing" | ||
|
|
||
| latest, err = fetch_latest_version() | ||
| if err: | ||
| return False, err | ||
|
|
||
| latest_full = latest or "" | ||
| stored_latest = latest_full[:STORED_TAG_MAX_LEN] if latest_full else None | ||
| current_str = str(current_version).lstrip("v") | ||
|
|
||
| current_v = normalize_version(current_str) | ||
| latest_v = normalize_version(latest_full) | ||
|
|
||
| try: | ||
| with transaction.atomic(): | ||
| try: | ||
| state, _ = UpdateCheckStatus.objects.select_for_update().get_or_create(pk=1) | ||
| except IntegrityError: | ||
| state = UpdateCheckStatus.objects.select_for_update().get(pk=1) | ||
|
|
||
| state.last_checked_at = now() | ||
|
|
||
| if not current_v or not latest_v: | ||
| if latest_full != current_str: | ||
| message = f"Update available: {latest_full} (current: {current_str})" | ||
| else: | ||
| message = f"IntelOwl version up to date ({current_str})" | ||
|
|
||
| state.save(update_fields=["last_checked_at"]) | ||
| return True, message | ||
|
|
||
| if latest_v > current_v: | ||
| message = f"New IntelOwl version available: {latest_full} (current: {current_str})" | ||
|
|
||
| should_notify = (state.latest_version != stored_latest) or (not state.notified) | ||
| if should_notify: | ||
| state.latest_version = stored_latest | ||
| state.notified = True | ||
| state.save(update_fields=["latest_version", "notified", "last_checked_at"]) | ||
|
|
||
| def _send_notification(): | ||
| _notify_admins( | ||
| "New IntelOwl version available", | ||
| f"Version {latest_full} is available (current: {current_str})", | ||
| ) | ||
|
|
||
| if getattr(settings, "TESTING", False): | ||
| _send_notification() | ||
| else: | ||
| transaction.on_commit(_send_notification) | ||
|
|
||
| logger.info( | ||
| "New IntelOwl version available (notified scheduled): %s (current: %s)", | ||
| latest_full, | ||
| current_str, | ||
| ) | ||
| else: | ||
| state.save(update_fields=["last_checked_at"]) | ||
|
|
||
| return True, message | ||
|
|
||
| if latest_v < current_v: | ||
| state.save(update_fields=["last_checked_at"]) | ||
| return ( | ||
| True, | ||
| f"Local version ahead of release: {current_str} > {latest_full}", | ||
| ) | ||
|
|
||
| state.save(update_fields=["last_checked_at"]) | ||
| return True, f"IntelOwl version up to date ({current_str})" | ||
|
|
||
| except Exception: | ||
| logger.exception("Unexpected error during update check (db/transaction)") | ||
| return False, "Unexpected error during update check. See logs for details." |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import logging | ||
|
|
||
| from django.core.management.base import BaseCommand, CommandError | ||
|
|
||
| from api_app.core.update_checker import check_for_update | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class Command(BaseCommand): | ||
| help = "Check for newer IntelOwl releases" | ||
|
|
||
| def handle(self, *args, **options): | ||
| try: | ||
| success, message = check_for_update() | ||
| except Exception: | ||
| logger.exception("Unexpected error during update check") | ||
| raise CommandError("Unexpected error during update check. See logs for details.") | ||
|
|
||
| if not success: | ||
| logger.info("Update check failed: %s", message) | ||
| raise CommandError(message) | ||
|
|
||
| self.stdout.write(self.style.SUCCESS(message)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| import json | ||
|
|
||
| import django.utils.timezone | ||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| def create_weekly_update_task(apps, schema_editor): | ||
| IntervalSchedule = apps.get_model("django_celery_beat", "IntervalSchedule") | ||
| PeriodicTask = apps.get_model("django_celery_beat", "PeriodicTask") | ||
|
|
||
| schedule, _ = IntervalSchedule.objects.get_or_create( | ||
| every=7, | ||
| period="days", | ||
| ) | ||
|
|
||
| PeriodicTask.objects.update_or_create( | ||
| name="Weekly IntelOwl Update Check", | ||
| defaults={ | ||
| "interval": schedule, | ||
| "task": "api_app.core.tasks.scheduled_update_check", | ||
| "kwargs": json.dumps({}), | ||
| "enabled": True, | ||
| }, | ||
| ) | ||
|
|
||
|
|
||
| def remove_weekly_update_task(apps, schema_editor): | ||
| PeriodicTask = apps.get_model("django_celery_beat", "PeriodicTask") | ||
| PeriodicTask.objects.filter(name="Weekly IntelOwl Update Check").delete() | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
| dependencies = [ | ||
| ("api_app", "0071_delete_last_elastic_report"), | ||
| ("django_celery_beat", "__latest__"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.CreateModel( | ||
| name="UpdateCheckStatus", | ||
| fields=[ | ||
| ( | ||
| "id", | ||
| models.BigAutoField( | ||
| auto_created=True, | ||
| primary_key=True, | ||
| serialize=False, | ||
| verbose_name="ID", | ||
| ), | ||
| ), | ||
| ( | ||
| "latest_version", | ||
| models.CharField( | ||
| max_length=20, | ||
| null=True, | ||
| blank=True, | ||
| help_text="Latest version detected during update check", | ||
| ), | ||
| ), | ||
| ( | ||
| "notified", | ||
| models.BooleanField( | ||
| default=False, | ||
| help_text="Whether notification has already been sent", | ||
| ), | ||
| ), | ||
| ( | ||
| "last_checked_at", | ||
| models.DateTimeField( | ||
| null=True, | ||
| blank=True, | ||
| help_text="Last time update check ran", | ||
| ), | ||
| ), | ||
| ( | ||
| "created_at", | ||
| models.DateTimeField( | ||
| default=django.utils.timezone.now, | ||
| editable=False, | ||
| ), | ||
| ), | ||
| ( | ||
| "updated_at", | ||
| models.DateTimeField(auto_now=True), | ||
| ), | ||
| ], | ||
| options={ | ||
| "verbose_name": "Update check info", | ||
| }, | ||
| ), | ||
| migrations.RunPython( | ||
| create_weekly_update_task, | ||
| remove_weekly_update_task, | ||
| ), | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| from django.conf import settings | ||
| from rest_framework import serializers | ||
|
|
||
| from api_app.core.update_checker import normalize_version | ||
| from api_app.models import UpdateCheckStatus | ||
|
|
||
|
|
||
| class SystemUpdateStatusSerializer(serializers.Serializer): | ||
| current_version = serializers.CharField() | ||
| latest_version = serializers.CharField(allow_null=True) | ||
| update_available = serializers.BooleanField() | ||
| last_checked_at = serializers.DateTimeField(allow_null=True) | ||
| notified = serializers.BooleanField() | ||
|
|
||
| @staticmethod | ||
| def from_state(state: UpdateCheckStatus | None): | ||
| current_version = str(getattr(settings, "INTEL_OWL_VERSION", "")).lstrip("v") | ||
|
|
||
| latest_version = state.latest_version if state else None | ||
| last_checked_at = state.last_checked_at if state else None | ||
| notified = state.notified if state else False | ||
|
|
||
| update_available = False | ||
| if latest_version and normalize_version(latest_version) > normalize_version(current_version): | ||
| update_available = True | ||
|
|
||
| return { | ||
| "current_version": current_version, | ||
| "latest_version": latest_version, | ||
| "update_available": update_available, | ||
| "last_checked_at": last_checked_at, | ||
| "notified": notified, | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there should also be a migration
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this has not been addressed