-
-
Notifications
You must be signed in to change notification settings - Fork 585
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
Changes from 11 commits
987765d
067ab3a
618f293
eef5c15
7c59a39
47ac868
0b12155
f07a054
21dd0e2
14c37da
bfce6ca
89eabf3
a7f3f40
95671b2
7b9fb40
024d0a2
ba4e056
0c9db74
8839e99
471dc63
c1681ba
82398e2
385ef00
7e17a87
bff38e5
26f956b
c570770
6f43f7e
034ea13
9bc6ee1
81a1e98
09f9796
eae37cf
a554277
a8dec4f
c1364d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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(name="intelowl.scheduled_update_check") | ||
| def scheduled_update_check(): | ||
| """ | ||
| Periodic task to check for IntelOwl updates. | ||
| Intended to be triggered via celery beat. | ||
| """ | ||
| check_for_update() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| import logging | ||
|
|
||
| import requests | ||
| from django.conf import settings | ||
| from django.utils.timezone import now | ||
|
|
||
| from api_app.models import UpdateCheckStatus | ||
| from api_app.user_events_manager.queryset import UserEventQuerySet | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def normalize_version(v): | ||
| """ | ||
| Convert '1.2.3' → (1, 2, 3) so versions can be compared. | ||
| Stops at the first non-numeric part. | ||
| """ | ||
| parts = [] | ||
| for x in v.split("."): | ||
| if x.isdigit(): | ||
| parts.append(int(x)) | ||
| else: | ||
| break | ||
| return tuple(parts) | ||
|
|
||
|
|
||
| def fetch_latest_version(): | ||
| """ | ||
| Fetch the latest IntelOwl version string from the update URL. | ||
| Returns a version string without any leading 'v', or None on error. | ||
| """ | ||
| update_url = getattr(settings, "UPDATE_CHECK_URL", None) | ||
|
|
||
| if not update_url: | ||
| return None, "UPDATE_CHECK_URL not configured" | ||
|
|
||
| try: | ||
| resp = requests.get( | ||
| update_url, | ||
| headers={"User-Agent": "IntelOwl-Update-Checker"}, | ||
| timeout=5, | ||
| ) | ||
| resp.raise_for_status() | ||
| except requests.RequestException as exc: | ||
| logger.error(f"update check failed: {exc}") | ||
| return None, "Failed to fetch release information" | ||
|
|
||
| try: | ||
| data = resp.json() | ||
| except ValueError: | ||
| logger.error("invalid JSON in update response") | ||
| return None, "Invalid response from update server" | ||
|
|
||
| tag = data.get("tag_name") | ||
| if not tag: | ||
| return None, "Release response missing tag_name" | ||
|
|
||
| return tag.lstrip("v"), None | ||
|
|
||
|
|
||
| def check_for_update(): | ||
| """ | ||
| Compare the running IntelOwl version with the latest available one. | ||
|
|
||
| Returns: | ||
| (success: bool, message: str) | ||
|
||
| """ | ||
| current_version_str = getattr(settings, "INTEL_OWL_VERSION", None) | ||
| if not current_version_str: | ||
| return False, "INTEL_OWL_VERSION setting missing" | ||
|
|
||
| latest_str, error = fetch_latest_version() | ||
| if error: | ||
| return False, error | ||
|
|
||
| current_version_str = str(current_version_str).lstrip("v") | ||
|
|
||
| current = normalize_version(current_version_str) | ||
| latest = normalize_version(latest_str) | ||
|
|
||
| state, _ = UpdateCheckStatus.objects.get_or_create(pk=1) | ||
| state.last_checked_at = now() | ||
|
|
||
| if not current or not latest: | ||
| state.save(update_fields=["last_checked_at"]) | ||
| if latest_str != current_version_str: | ||
| return ( | ||
| True, | ||
| f"Update available: {latest_str} (current: {current_version_str})", | ||
| ) | ||
| return True, f"IntelOwl version up to date ({current_version_str})" | ||
|
|
||
| if latest > current: | ||
| if state.latest_version != latest_str or not state.notified: | ||
| logger.warning( | ||
|
||
| "New IntelOwl version available: %s (current: %s)", | ||
| latest_str, | ||
| current_version_str, | ||
| ) | ||
|
|
||
| UserEventQuerySet.notify_admins( | ||
| title="New IntelOwl version available", | ||
| message=( | ||
| f"Version {latest_str} is available " | ||
| f"(current: {current_version_str})" | ||
| ), | ||
| persistent=True, | ||
| severity="warning", | ||
|
||
| ) | ||
|
Comment on lines
73
to
78
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this create duplicate notifications if the command in ran twice ? You can check if the version has already been notified. |
||
|
|
||
| state.latest_version = latest_str | ||
| state.notified = True | ||
|
|
||
| state.save(update_fields=["latest_version", "notified", "last_checked_at"]) | ||
| return ( | ||
| True, | ||
| f"New IntelOwl version available: {latest_str} " | ||
| f"(current: {current_version_str})", | ||
| ) | ||
|
|
||
| state.save(update_fields=["last_checked_at"]) | ||
|
||
|
|
||
| if latest < current: | ||
| return ( | ||
| True, | ||
| f"Local version ahead of release: " f"{current_version_str} > {latest_str}", | ||
| ) | ||
|
|
||
| return True, f"IntelOwl version up to date ({current_version_str})" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| from django.core.management.base import BaseCommand | ||
|
|
||
| from api_app.core.update_checker import check_for_update | ||
|
|
||
|
|
||
| class Command(BaseCommand): | ||
| help = "Check for newer IntelOwl releases" | ||
|
|
||
| def handle(self, *args, **options): | ||
| success, message = check_for_update() | ||
|
|
||
| if success: | ||
| self.stdout.write(self.style.SUCCESS(message)) | ||
| else: | ||
| self.stdout.write(self.style.ERROR(message)) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -67,6 +67,44 @@ | |
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class UpdateCheckStatus(models.Model): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there should also be a migration
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this has not been addressed |
||
| """ | ||
| Stores global state for IntelOwl update checks. | ||
| This model is intended to be used as a singleton (accessed via get_or_create(pk=1)). | ||
| Ensures that update notifications are emitted only once per version. | ||
| """ | ||
|
|
||
| latest_version = models.CharField( | ||
| max_length=50, | ||
|
||
| null=True, | ||
| blank=True, | ||
| help_text="Latest version detected during update check", | ||
| ) | ||
| notified = models.BooleanField( | ||
| default=False, | ||
| help_text="Whether a notification has already been sent for this version", | ||
| ) | ||
| last_checked_at = models.DateTimeField( | ||
| null=True, | ||
| blank=True, | ||
| help_text="Last time the update check was executed", | ||
| ) | ||
|
|
||
| created_at = models.DateTimeField(default=now, editable=False) | ||
| updated_at = models.DateTimeField(auto_now=True) | ||
|
|
||
| class Meta: | ||
| verbose_name = "Update check status" | ||
| verbose_name_plural = "Update check status" | ||
|
||
|
|
||
| def __str__(self) -> str: | ||
| return ( | ||
| f"UpdateCheckStatus(" | ||
| f"latest_version={self.latest_version}, " | ||
| f"notified={self.notified})" | ||
| ) | ||
|
|
||
|
|
||
| class PythonModule(models.Model): | ||
| """ | ||
| Represents a Python module model used in the application. | ||
|
|
||
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.
before putting this into a cron, we need to add also unittests for all the possible cases here.