plugins.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  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 django.utils.translation import gettext_lazy as _
  10. from netbox.plugins import PluginConfig
  11. from utilities.datetime import datetime_from_timestamp
  12. USER_AGENT_STRING = f'NetBox/{settings.RELEASE.version} {settings.RELEASE.edition}'
  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():
  61. """
  62. Return a dictionary of all locally-installed plugins, mapped by name.
  63. """
  64. plugins = {}
  65. for plugin_name in settings.PLUGINS:
  66. plugin = importlib.import_module(plugin_name)
  67. plugin_config: PluginConfig = plugin.config
  68. plugins[plugin_config.name] = Plugin(
  69. slug=plugin_config.name,
  70. title_short=plugin_config.verbose_name,
  71. tag_line=plugin_config.description,
  72. description_short=plugin_config.description,
  73. is_local=True,
  74. is_installed=True,
  75. installed_version=plugin_config.version,
  76. )
  77. return plugins
  78. def get_catalog_plugins():
  79. """
  80. Return a dictionary of all entries in the plugins catalog, mapped by name.
  81. """
  82. session = requests.Session()
  83. plugins = {}
  84. def get_pages():
  85. # TODO: pagination is currently broken in API
  86. payload = {'page': '1', 'per_page': '50'}
  87. first_page = session.get(
  88. settings.PLUGIN_CATALOG_URL,
  89. headers={'User-Agent': USER_AGENT_STRING},
  90. proxies=settings.HTTP_PROXIES,
  91. timeout=3,
  92. params=payload
  93. ).json()
  94. yield first_page
  95. num_pages = first_page['metadata']['pagination']['last_page']
  96. for page in range(2, num_pages + 1):
  97. payload['page'] = page
  98. next_page = session.get(
  99. settings.PLUGIN_CATALOG_URL,
  100. headers={'User-Agent': USER_AGENT_STRING},
  101. proxies=settings.HTTP_PROXIES,
  102. timeout=3,
  103. params=payload
  104. ).json()
  105. yield next_page
  106. for page in get_pages():
  107. for data in page['data']:
  108. # Populate releases
  109. releases = []
  110. for version in data['release_recent_history']:
  111. releases.append(
  112. PluginVersion(
  113. date=datetime_from_timestamp(version['date']),
  114. version=version['version'],
  115. netbox_min_version=version['netbox_min_version'],
  116. netbox_max_version=version['netbox_max_version'],
  117. has_model=version['has_model'],
  118. is_certified=version['is_certified'],
  119. is_feature=version['is_feature'],
  120. is_integration=version['is_integration'],
  121. is_netboxlabs_supported=version['is_netboxlabs_supported'],
  122. )
  123. )
  124. releases = sorted(releases, key=lambda x: x.date, reverse=True)
  125. latest_release = PluginVersion(
  126. date=datetime_from_timestamp(data['release_latest']['date']),
  127. version=data['release_latest']['version'],
  128. netbox_min_version=data['release_latest']['netbox_min_version'],
  129. netbox_max_version=data['release_latest']['netbox_max_version'],
  130. has_model=data['release_latest']['has_model'],
  131. is_certified=data['release_latest']['is_certified'],
  132. is_feature=data['release_latest']['is_feature'],
  133. is_integration=data['release_latest']['is_integration'],
  134. is_netboxlabs_supported=data['release_latest']['is_netboxlabs_supported'],
  135. )
  136. # Populate author (if any)
  137. if data['author']:
  138. author = PluginAuthor(
  139. name=data['author']['name'],
  140. org_id=data['author']['org_id'],
  141. url=data['author']['url'],
  142. )
  143. else:
  144. author = None
  145. # Populate plugin data
  146. plugins[data['slug']] = Plugin(
  147. id=data['id'],
  148. status=data['status'],
  149. title_short=data['title_short'],
  150. title_long=data['title_long'],
  151. tag_line=data['tag_line'],
  152. description_short=data['description_short'],
  153. slug=data['slug'],
  154. author=author,
  155. created_at=datetime_from_timestamp(data['created_at']),
  156. updated_at=datetime_from_timestamp(data['updated_at']),
  157. license_type=data['license_type'],
  158. homepage_url=data['homepage_url'],
  159. package_name_pypi=data['package_name_pypi'],
  160. config_name=data['config_name'],
  161. is_certified=data['is_certified'],
  162. release_latest=latest_release,
  163. release_recent_history=releases,
  164. )
  165. return plugins
  166. def get_plugins():
  167. """
  168. Return a dictionary of all plugins (both catalog and locally installed), mapped by name.
  169. """
  170. local_plugins = get_local_plugins()
  171. catalog_plugins = cache.get('plugins-catalog-feed')
  172. if not catalog_plugins:
  173. catalog_plugins = get_catalog_plugins()
  174. cache.set('plugins-catalog-feed', catalog_plugins, 3600)
  175. plugins = catalog_plugins
  176. for k, v in local_plugins.items():
  177. if k in plugins:
  178. plugins[k].is_local = True
  179. plugins[k].is_installed = True
  180. else:
  181. plugins[k] = v
  182. return plugins