plugins.py 8.6 KB

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