plugins.py 8.6 KB

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