Skip to content
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

14731 plugins catalog #16763

Merged
merged 47 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
6953c5e
14731 plugin catalog
arthanson Jun 27, 2024
af601c5
14731 detal page
arthanson Jun 27, 2024
1a33b28
14731 plugin table
arthanson Jun 27, 2024
1e26ae7
14731 cleanup
arthanson Jun 28, 2024
0143a55
14731 cache API results
arthanson Jun 28, 2024
7be752a
14731 fix install name
arthanson Jul 8, 2024
577e9c7
14731 filtering
arthanson Jul 8, 2024
92fac68
Merge branch 'feature' into 14731-plugins-catalog
arthanson Jul 8, 2024
3eacf36
14731 filtering
arthanson Jul 8, 2024
c38fa7f
14731 fix detail view
arthanson Jul 8, 2024
6110611
14731 fix detail view
arthanson Jul 9, 2024
36e2683
14731 sort / status
arthanson Jul 9, 2024
d2945c9
14731 sort / status
arthanson Jul 9, 2024
a2b86c5
14731 cleanup detail view
arthanson Jul 9, 2024
cfd4a17
14731 htmx plugin list
arthanson Jul 9, 2024
486b10e
14731 align quicksearch
arthanson Jul 10, 2024
fa7c4cc
Merge branch 'feature' into 14731-plugins-catalog
arthanson Jul 11, 2024
7d3cf49
14731 remove pytz
arthanson Jul 11, 2024
6136154
Merge branch 'feature' into 14731-plugins-catalog
arthanson Jul 15, 2024
e9dd154
14731 change to table
arthanson Jul 15, 2024
578056e
14731 change to table
arthanson Jul 15, 2024
d15d532
14731 remove status from table
arthanson Jul 15, 2024
9495518
14731 quick search
arthanson Jul 15, 2024
83a0d5d
14731 cleanup
arthanson Jul 15, 2024
8b10c83
14731 cleanup
arthanson Jul 15, 2024
7e7572a
Merge branch 'feature' into 14731-plugins-catalog
arthanson Jul 16, 2024
dbcc29e
Merge branch 'feature' into 14731-plugins-catalog
jeremystretch Jul 16, 2024
dd90cca
Employ datetime_from_timestamp() to parse timestamps
jeremystretch Jul 16, 2024
83a0a55
14731 review changes
arthanson Jul 17, 2024
b79b3b6
14731 move to plugins.py file
arthanson Jul 17, 2024
657b12c
14731 use dataclasses
arthanson Jul 17, 2024
7594ecc
14731 review changes
arthanson Jul 17, 2024
fcbb8c9
Tweak table columns
jeremystretch Jul 17, 2024
6228766
Use is_staff (for now) to evaluate user permission for plugin views
jeremystretch Jul 17, 2024
b31eb03
Use table for ordering
jeremystretch Jul 17, 2024
3964e81
7025 change to api fields
arthanson Jul 18, 2024
1f588af
14731 merge
arthanson Jul 18, 2024
28bc43e
14731 tweaks
arthanson Jul 18, 2024
d2b6c3b
Remove filtering for is_netboxlabs_supported
jeremystretch Jul 22, 2024
e52c9af
Misc cleanup
jeremystretch Jul 22, 2024
2d8732f
Merge branch 'feature' into 14731-plugins-catalog
jeremystretch Jul 22, 2024
3faf6df
Update logic for determining whether to display plugin installation i…
jeremystretch Jul 22, 2024
56fb538
14731 review changes
arthanson Jul 23, 2024
845bfbd
14731 review changes
arthanson Jul 23, 2024
6b16388
14731 review changes
arthanson Jul 24, 2024
54e4a9f
14731 add user agent string, proxy settings
arthanson Jul 25, 2024
3e33b2c
Clean up templates
jeremystretch Jul 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 195 additions & 0 deletions netbox/core/plugins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import datetime
import importlib
import importlib.util
from dataclasses import dataclass, field
from typing import Optional

import requests
from django.conf import settings
from django.core.cache import cache
from django.utils.translation import gettext_lazy as _

from netbox.plugins import PluginConfig
from utilities.datetime import datetime_from_timestamp


@dataclass
class PluginAuthor:
"""
Identifying information for the author of a plugin.
"""
name: str
org_id: str = ''
url: str = ''


@dataclass
class PluginVersion:
"""
Details for a specific versioned release of a plugin.
"""
date: datetime.datetime = None
version: str = ''
netbox_min_version: str = ''
netbox_max_version: str = ''
has_model: bool = False
is_certified: bool = False
is_feature: bool = False
is_integration: bool = False
is_netboxlabs_supported: bool = False


@dataclass
class Plugin:
"""
The representation of a NetBox plugin in the catalog API.
"""
id: str = ''
status: str = ''
title_short: str = ''
title_long: str = ''
tag_line: str = ''
description_short: str = ''
slug: str = ''
author: Optional[PluginAuthor] = None
created_at: datetime.datetime = None
updated_at: datetime.datetime = None
license_type: str = ''
homepage_url: str = ''
package_name_pypi: str = ''
config_name: str = ''
is_certified: bool = False
release_latest: PluginVersion = field(default_factory=PluginVersion)
release_recent_history: list[PluginVersion] = field(default_factory=list)
is_local: bool = False # extra field for locally installed plugins
is_installed: bool = False
installed_version: str = ''


def get_local_plugins():
"""
Return a dictionary of all locally-installed plugins, mapped by name.
"""
plugins = {}
for plugin_name in settings.PLUGINS:
plugin = importlib.import_module(plugin_name)
plugin_config: PluginConfig = plugin.config

plugins[plugin_config.name] = Plugin(
slug=plugin_config.name,
title_short=plugin_config.verbose_name,
tag_line=plugin_config.description,
description_short=plugin_config.description,
is_local=True,
is_installed=True,
installed_version=plugin_config.version,
)

return plugins


def get_catalog_plugins():
"""
Return a dictionary of all entries in the plugins catalog, mapped by name.
"""
session = requests.Session()
plugins = {}

def get_pages():
# TODO: pagination is currently broken in API
payload = {'page': '1', 'per_page': '50'}
first_page = session.get(settings.PLUGIN_CATALOG_URL, params=payload).json()
yield first_page
num_pages = first_page['metadata']['pagination']['last_page']

for page in range(2, num_pages + 1):
payload['page'] = page
next_page = session.get(settings.PLUGIN_CATALOG_URL, params=payload).json()
yield next_page

for page in get_pages():
for data in page['data']:

# Populate releases
releases = []
for version in data['release_recent_history']:
releases.append(
PluginVersion(
date=datetime_from_timestamp(version['date']),
version=version['version'],
netbox_min_version=version['netbox_min_version'],
netbox_max_version=version['netbox_max_version'],
has_model=version['has_model'],
is_certified=version['is_certified'],
is_feature=version['is_feature'],
is_integration=version['is_integration'],
is_netboxlabs_supported=version['is_netboxlabs_supported'],
)
)
releases = sorted(releases, key=lambda x: x.date, reverse=True)
latest_release = PluginVersion(
date=datetime_from_timestamp(data['release_latest']['date']),
version=data['release_latest']['version'],
netbox_min_version=data['release_latest']['netbox_min_version'],
netbox_max_version=data['release_latest']['netbox_max_version'],
has_model=data['release_latest']['has_model'],
is_certified=data['release_latest']['is_certified'],
is_feature=data['release_latest']['is_feature'],
is_integration=data['release_latest']['is_integration'],
is_netboxlabs_supported=data['release_latest']['is_netboxlabs_supported'],
)

# Populate author (if any)
if data['author']:
print(data['author'])
author = PluginAuthor(
name=data['author']['name'],
org_id=data['author']['org_id'],
url=data['author']['url'],
)
else:
author = None

# Populate plugin data
plugins[data['slug']] = Plugin(
id=data['id'],
status=data['status'],
title_short=data['title_short'],
title_long=data['title_long'],
tag_line=data['tag_line'],
description_short=data['description_short'],
slug=data['slug'],
author=author,
created_at=datetime_from_timestamp(data['created_at']),
updated_at=datetime_from_timestamp(data['updated_at']),
license_type=data['license_type'],
homepage_url=data['homepage_url'],
package_name_pypi=data['package_name_pypi'],
config_name=data['config_name'],
is_certified=data['is_certified'],
release_latest=latest_release,
release_recent_history=releases,
)

return plugins


def get_plugins():
"""
Return a dictionary of all plugins (both catalog and locally installed), mapped by name.
"""
local_plugins = get_local_plugins()
catalog_plugins = cache.get('plugins-catalog-feed')
if not catalog_plugins:
catalog_plugins = get_catalog_plugins()
cache.set('plugins-catalog-feed', catalog_plugins, 3600)

plugins = catalog_plugins
for k, v in local_plugins.items():
if k in plugins:
plugins[k].is_local = True
plugins[k].is_installed = True
else:
plugins[k] = v

return plugins
75 changes: 58 additions & 17 deletions netbox/core/tables/plugins.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,80 @@
import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
from netbox.tables import BaseTable

from netbox.tables import BaseTable, columns

__all__ = (
'PluginTable',
'CatalogPluginTable',
'PluginVersionTable',
)


class PluginTable(BaseTable):
name = tables.Column(
accessor=tables.A('verbose_name'),
verbose_name=_('Name')
)
class PluginVersionTable(BaseTable):
version = tables.Column(
verbose_name=_('Version')
)
package = tables.Column(
accessor=tables.A('name'),
verbose_name=_('Package')
last_updated = columns.DateTimeColumn(
accessor=tables.A('date'),
timespec='minutes',
verbose_name=_('Last Updated')
)
min_version = tables.Column(
accessor=tables.A('netbox_min_version'),
verbose_name=_('Minimum NetBox Version')
)
max_version = tables.Column(
accessor=tables.A('netbox_max_version'),
verbose_name=_('Maximum NetBox Version')
)

class Meta(BaseTable.Meta):
empty_text = _('No plugin data found')
fields = (
'version', 'last_updated', 'min_version', 'max_version',
)
default_columns = (
'version', 'last_updated', 'min_version', 'max_version',
)
orderable = False


class CatalogPluginTable(BaseTable):
title_short = tables.Column(
linkify=('core:plugin', [tables.A('slug')]),
verbose_name=_('Name')
)
author = tables.Column(
accessor=tables.A('author.name'),
verbose_name=_('Author')
)
author_email = tables.Column(
verbose_name=_('Author Email')
is_local = columns.BooleanColumn(
verbose_name=_('Local')
)
is_installed = columns.BooleanColumn(
verbose_name=_('Installed')
)
is_certified = columns.BooleanColumn(
verbose_name=_('Certified')
)
created_at = columns.DateTimeColumn(
verbose_name=_('Published')
)
updated_at = columns.DateTimeColumn(
verbose_name=_('Updated')
)
description = tables.Column(
verbose_name=_('Description')
installed_version = tables.Column(
verbose_name=_('Installed version')
)

class Meta(BaseTable.Meta):
empty_text = _('No plugins found')
empty_text = _('No plugin data found')
fields = (
'name', 'version', 'package', 'author', 'author_email', 'description',
'title_short', 'author', 'is_local', 'is_installed', 'is_certified', 'created_at', 'updated_at',
'installed_version',
)
default_columns = (
'name', 'version', 'package', 'description',
'title_short', 'author', 'is_local', 'is_installed', 'is_certified', 'created_at', 'updated_at',
)
# List installed plugins first, then certified plugins, then
# everything else (with each tranche ordered alphabetically)
order_by = ('-is_installed', '-is_certified', 'name')
4 changes: 4 additions & 0 deletions netbox/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,8 @@

# System
path('system/', views.SystemView.as_view(), name='system'),

# Plugins
path('plugins/', views.PluginListView.as_view(), name='plugin_list'),
path('plugins/<str:name>/', views.PluginView.as_view(), name='plugin'),
)
Loading