plugins.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import datetime
  2. import importlib
  3. import importlib.util
  4. from dataclasses import dataclass, field
  5. from typing import Optional
  6. import requests
  7. from django.conf import settings
  8. from django.core.cache import cache
  9. from netbox.plugins import PluginConfig
  10. from utilities.datetime import datetime_from_timestamp
  11. USER_AGENT_STRING = f'NetBox/{settings.RELEASE.version} {settings.RELEASE.edition}'
  12. CACHE_KEY_CATALOG_FEED = 'plugins-catalog-feed'
  13. @dataclass
  14. class PluginAuthor:
  15. """
  16. Identifying information for the author of a plugin.
  17. """
  18. name: str
  19. org_id: str = ''
  20. url: str = ''
  21. @dataclass
  22. class PluginVersion:
  23. """
  24. Details for a specific versioned release of a plugin.
  25. """
  26. date: datetime.datetime = None
  27. version: str = ''
  28. netbox_min_version: str = ''
  29. netbox_max_version: str = ''
  30. has_model: bool = False
  31. is_certified: bool = False
  32. is_feature: bool = False
  33. is_integration: bool = False
  34. is_netboxlabs_supported: bool = False
  35. @dataclass
  36. class Plugin:
  37. """
  38. The representation of a NetBox plugin in the catalog API.
  39. """
  40. id: str = ''
  41. status: str = ''
  42. title_short: str = ''
  43. title_long: str = ''
  44. tag_line: str = ''
  45. description_short: str = ''
  46. slug: str = ''
  47. author: Optional[PluginAuthor] = None
  48. created_at: datetime.datetime = None
  49. updated_at: datetime.datetime = None
  50. license_type: str = ''
  51. homepage_url: str = ''
  52. package_name_pypi: str = ''
  53. config_name: str = ''
  54. is_certified: bool = False
  55. release_latest: PluginVersion = field(default_factory=PluginVersion)
  56. release_recent_history: list[PluginVersion] = field(default_factory=list)
  57. is_local: bool = False # extra field for locally installed plugins
  58. is_installed: bool = False
  59. installed_version: str = ''
  60. def get_local_plugins(plugins=None):
  61. """
  62. Return a dictionary of all locally-installed plugins, mapped by name.
  63. """
  64. plugins = plugins or {}
  65. local_plugins = {}
  66. # Gather all locally-installed plugins
  67. for plugin_name in settings.PLUGINS:
  68. plugin = importlib.import_module(plugin_name)
  69. plugin_config: PluginConfig = plugin.config
  70. local_plugins[plugin_config.name] = Plugin(
  71. config_name=plugin_config.name,
  72. title_short=plugin_config.verbose_name,
  73. title_long=plugin_config.verbose_name,
  74. tag_line=plugin_config.description,
  75. description_short=plugin_config.description,
  76. is_local=True,
  77. is_installed=True,
  78. installed_version=plugin_config.version,
  79. )
  80. # Update catalog entries for local plugins, or add them to the list if not listed
  81. for k, v in local_plugins.items():
  82. if k in plugins:
  83. plugins[k].is_local = True
  84. plugins[k].is_installed = True
  85. plugins[k].installed_version = v.installed_version
  86. else:
  87. plugins[k] = v
  88. return plugins
  89. def get_catalog_plugins():
  90. """
  91. Return a dictionary of all entries in the plugins catalog, mapped by name.
  92. """
  93. session = requests.Session()
  94. # Disable catalog fetching for isolated deployments
  95. if settings.ISOLATED_DEPLOYMENT:
  96. return {}
  97. def get_pages():
  98. # TODO: pagination is currently broken in API
  99. payload = {'page': '1', 'per_page': '50'}
  100. first_page = session.get(
  101. settings.PLUGIN_CATALOG_URL,
  102. headers={'User-Agent': USER_AGENT_STRING},
  103. proxies=settings.HTTP_PROXIES,
  104. timeout=3,
  105. params=payload
  106. ).json()
  107. yield first_page
  108. num_pages = first_page['metadata']['pagination']['last_page']
  109. for page in range(2, num_pages + 1):
  110. payload['page'] = page
  111. next_page = session.get(
  112. settings.PLUGIN_CATALOG_URL,
  113. headers={'User-Agent': USER_AGENT_STRING},
  114. proxies=settings.HTTP_PROXIES,
  115. timeout=3,
  116. params=payload
  117. ).json()
  118. yield next_page
  119. def make_plugin_dict():
  120. plugins = {}
  121. for page in get_pages():
  122. for data in page['data']:
  123. # Populate releases
  124. releases = []
  125. for version in data['release_recent_history']:
  126. releases.append(
  127. PluginVersion(
  128. date=datetime_from_timestamp(version['date']),
  129. version=version['version'],
  130. netbox_min_version=version['netbox_min_version'],
  131. netbox_max_version=version['netbox_max_version'],
  132. has_model=version['has_model'],
  133. is_certified=version['is_certified'],
  134. is_feature=version['is_feature'],
  135. is_integration=version['is_integration'],
  136. is_netboxlabs_supported=version['is_netboxlabs_supported'],
  137. )
  138. )
  139. releases = sorted(releases, key=lambda x: x.date, reverse=True)
  140. latest_release = PluginVersion(
  141. date=datetime_from_timestamp(data['release_latest']['date']),
  142. version=data['release_latest']['version'],
  143. netbox_min_version=data['release_latest']['netbox_min_version'],
  144. netbox_max_version=data['release_latest']['netbox_max_version'],
  145. has_model=data['release_latest']['has_model'],
  146. is_certified=data['release_latest']['is_certified'],
  147. is_feature=data['release_latest']['is_feature'],
  148. is_integration=data['release_latest']['is_integration'],
  149. is_netboxlabs_supported=data['release_latest']['is_netboxlabs_supported'],
  150. )
  151. # Populate author (if any)
  152. if data['author']:
  153. author = PluginAuthor(
  154. name=data['author']['name'],
  155. org_id=data['author']['org_id'],
  156. url=data['author']['url'],
  157. )
  158. else:
  159. author = None
  160. # Populate plugin data
  161. plugins[data['config_name']] = Plugin(
  162. id=data['id'],
  163. status=data['status'],
  164. title_short=data['title_short'],
  165. title_long=data['title_long'],
  166. tag_line=data['tag_line'],
  167. description_short=data['description_short'],
  168. slug=data['slug'],
  169. author=author,
  170. created_at=datetime_from_timestamp(data['created_at']),
  171. updated_at=datetime_from_timestamp(data['updated_at']),
  172. license_type=data['license_type'],
  173. homepage_url=data['homepage_url'],
  174. package_name_pypi=data['package_name_pypi'],
  175. config_name=data['config_name'],
  176. is_certified=data['is_certified'],
  177. release_latest=latest_release,
  178. release_recent_history=releases,
  179. )
  180. return plugins
  181. catalog_plugins = cache.get(CACHE_KEY_CATALOG_FEED, default={})
  182. if not catalog_plugins:
  183. try:
  184. catalog_plugins = make_plugin_dict()
  185. cache.set(CACHE_KEY_CATALOG_FEED, catalog_plugins, 3600)
  186. except requests.exceptions.RequestException:
  187. pass
  188. return catalog_plugins