diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2bef95d..5e18717 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: test: runs-on: ubuntu-latest - name: Runs plugin tests - NetBox v3.5 + name: Runs plugin tests steps: - name: Checkout diff --git a/README.md b/README.md index 7a1c47b..795b827 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # NetBox Cloud Pilot -[NetBox Cloud Pilot](https://github.com/Onemind-Services-LLC/netbox-cloud-pilot) is a specialized plugin tailored for -NetBox, offering enhanced functionality for users on the -[CloudMyDC public Cloud platform (VAP)](https://app.xapp.cloudmydc.com/). This plugin simplifies the management of +[NetBox Cloud Pilot](https://github.com/Onemind-Services-LLC/netbox-cloud-pilot) is a specialized plugin tailored for +NetBox, offering enhanced functionality for users on the +[CloudMyDC public Cloud platform (VAP)](https://app.xapp.cloudmydc.com/). This plugin simplifies the management of various NetBox components and infrastructure, streamlining processes and offering a more integrated experience. -NetBox Cloud Pilot, developed and maintained by [Onemind Services LLC](https://onemindservices.com/), introduces an -exclusive plugin store for NetBox. It caters to users with a diverse range of both free and paid plugins, enhancing the +NetBox Cloud Pilot, developed and maintained by [Onemind Services LLC](https://onemindservices.com/), introduces an +exclusive plugin store for NetBox. It caters to users with a diverse range of both free and paid plugins, enhancing the NetBox experience. ## Features @@ -35,5 +35,5 @@ For support, questions, or feedback, please file an issue on our [GitHub issue t ## Contributing -Contributions to NetBox Cloud Pilot are welcome! If you'd like to contribute, please fork the repository and submit a +Contributions to NetBox Cloud Pilot are welcome! If you'd like to contribute, please fork the repository and submit a pull request. For more detailed information, refer to our contributing guidelines in the repository. diff --git a/configuration/configuration.py b/configuration/configuration.py index c28e838..f5ae2cd 100644 --- a/configuration/configuration.py +++ b/configuration/configuration.py @@ -49,10 +49,7 @@ def _read_secret(secret_name, default=None): # Database connection SSLMODE "CONN_MAX_AGE": int(environ.get("DB_CONN_MAX_AGE", "300")), # Max database connection age - "DISABLE_SERVER_SIDE_CURSORS": environ.get( - "DB_DISABLE_SERVER_SIDE_CURSORS", "False" - ).lower() - == "true", + "DISABLE_SERVER_SIDE_CURSORS": environ.get("DB_DISABLE_SERVER_SIDE_CURSORS", "False").lower() == "true", # Disable the use of server-side cursors transaction pooling } @@ -66,10 +63,7 @@ def _read_secret(secret_name, default=None): "PASSWORD": _read_secret("redis_password", environ.get("REDIS_PASSWORD", "")), "DATABASE": int(environ.get("REDIS_DATABASE", 0)), "SSL": environ.get("REDIS_SSL", "False").lower() == "true", - "INSECURE_SKIP_TLS_VERIFY": environ.get( - "REDIS_INSECURE_SKIP_TLS_VERIFY", "False" - ).lower() - == "true", + "INSECURE_SKIP_TLS_VERIFY": environ.get("REDIS_INSECURE_SKIP_TLS_VERIFY", "False").lower() == "true", }, "caching": { "HOST": environ.get("REDIS_CACHE_HOST", environ.get("REDIS_HOST", "localhost")), @@ -79,8 +73,7 @@ def _read_secret(secret_name, default=None): environ.get("REDIS_CACHE_PASSWORD", environ.get("REDIS_PASSWORD", "")), ), "DATABASE": int(environ.get("REDIS_CACHE_DATABASE", 1)), - "SSL": environ.get("REDIS_CACHE_SSL", environ.get("REDIS_SSL", "False")).lower() - == "true", + "SSL": environ.get("REDIS_CACHE_SSL", environ.get("REDIS_SSL", "False")).lower() == "true", "INSECURE_SKIP_TLS_VERIFY": environ.get( "REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY", environ.get("REDIS_INSECURE_SKIP_TLS_VERIFY", "False"), diff --git a/netbox_cloud_pilot/__init__.py b/netbox_cloud_pilot/__init__.py index d6d2669..2f203dc 100644 --- a/netbox_cloud_pilot/__init__.py +++ b/netbox_cloud_pilot/__init__.py @@ -7,8 +7,8 @@ class NetBoxCloudPilot(PluginConfig): name = metadata.get("Name").replace("-", "_") - verbose_name = metadata.get("Summary") - description = metadata.get("Long-Description") + verbose_name = metadata.get("Name") + description = metadata.get("Summary") version = metadata.get("Version") author = metadata.get("Author") author_email = metadata.get("Author-email") diff --git a/netbox_cloud_pilot/constants.py b/netbox_cloud_pilot/constants.py index 2a6b2eb..3cf6e8a 100644 --- a/netbox_cloud_pilot/constants.py +++ b/netbox_cloud_pilot/constants.py @@ -5,9 +5,7 @@ from .nb_settings import * JELASTIC_API = "https://app.xapp.cloudmydc.com" -NETBOX_JPS_REPO = ( - "https://raw.githubusercontent.com/Onemind-Services-LLC/netbox-jps/master" -) +NETBOX_JPS_REPO = "https://raw.githubusercontent.com/Onemind-Services-LLC/netbox-jps/master" NODE_GROUP_CP = "cp" NODE_GROUP_SQLDB = "sqldb" @@ -174,7 +172,7 @@ help_text="Base path for URL patterns", placeholder="/", required=False, - initial="/", + initial=None, ), Param( key="EMAIL_FROM", @@ -296,9 +294,7 @@ placeholder="{'filter_name': 'path.to.filter'}", required=False, field=forms.JSONField, - field_kwargs={ - "widget": forms.Textarea(attrs={"class": "vLargeTextField"}) - }, + field_kwargs={"widget": forms.Textarea(attrs={"class": "vLargeTextField"})}, initial={}, ), Param( @@ -336,9 +332,7 @@ placeholder="{'location': '/var/netbox/media'}", required=False, field=forms.JSONField, - field_kwargs={ - "widget": forms.Textarea(attrs={"class": "vLargeTextField"}) - }, + field_kwargs={"widget": forms.Textarea(attrs={"class": "vLargeTextField"})}, initial={}, ), ], @@ -372,9 +366,7 @@ placeholder="[]", required=False, field=forms.JSONField, - field_kwargs={ - "widget": forms.Textarea(attrs={"class": "vLargeTextField"}) - }, + field_kwargs={"widget": forms.Textarea(attrs={"class": "vLargeTextField"})}, initial=[ [ { @@ -500,9 +492,7 @@ help_text="List of custom data validators", placeholder="[]", required=False, - field_kwargs={ - "widget": forms.Textarea(attrs={"class": "vLargeTextField"}) - }, + field_kwargs={"widget": forms.Textarea(attrs={"class": "vLargeTextField"})}, ), Param( key="FIELD_CHOICES", @@ -510,9 +500,7 @@ help_text="List of custom field choices", placeholder="[]", required=False, - field_kwargs={ - "widget": forms.Textarea(attrs={"class": "vLargeTextField"}) - }, + field_kwargs={"widget": forms.Textarea(attrs={"class": "vLargeTextField"})}, ), ], ), @@ -525,9 +513,7 @@ help_text="Default dashboard for users", required=False, field=forms.JSONField, - field_kwargs={ - "widget": forms.Textarea(attrs={"class": "vLargeTextField"}) - }, + field_kwargs={"widget": forms.Textarea(attrs={"class": "vLargeTextField"})}, initial=DEFAULT_DASHBOARD, ), Param( @@ -536,9 +522,7 @@ help_text="Default user preferences", required=False, field=forms.JSONField, - field_kwargs={ - "widget": forms.Textarea(attrs={"class": "vLargeTextField"}) - }, + field_kwargs={"widget": forms.Textarea(attrs={"class": "vLargeTextField"})}, initial={}, ), Param( @@ -654,9 +638,7 @@ help_text="List of NetBox administrators", placeholder="[('NetBox Admin', 'admin@example.com')]", required=False, - field_kwargs={ - "widget": forms.Textarea(attrs={"class": "vLargeTextField"}) - }, + field_kwargs={"widget": forms.Textarea(attrs={"class": "vLargeTextField"})}, ), Param( key="BANNER_BOTTOM", @@ -664,9 +646,7 @@ help_text="Bottom banner text", placeholder="Banner text", required=False, - field_kwargs={ - "widget": forms.Textarea(attrs={"class": "vLargeTextField"}) - }, + field_kwargs={"widget": forms.Textarea(attrs={"class": "vLargeTextField"})}, ), Param( key="BANNER_LOGIN", @@ -674,9 +654,7 @@ help_text="Login banner text", placeholder="Banner text", required=False, - field_kwargs={ - "widget": forms.Textarea(attrs={"class": "vLargeTextField"}) - }, + field_kwargs={"widget": forms.Textarea(attrs={"class": "vLargeTextField"})}, ), Param( key="BANNER_TOP", @@ -684,9 +662,7 @@ help_text="Top banner text", placeholder="Banner text", required=False, - field_kwargs={ - "widget": forms.Textarea(attrs={"class": "vLargeTextField"}) - }, + field_kwargs={"widget": forms.Textarea(attrs={"class": "vLargeTextField"})}, ), Param( key="BANNER_MAINTENANCE", @@ -694,9 +670,7 @@ help_text="Maintenance banner text", placeholder="Banner text", required=False, - field_kwargs={ - "widget": forms.Textarea(attrs={"class": "vLargeTextField"}) - }, + field_kwargs={"widget": forms.Textarea(attrs={"class": "vLargeTextField"})}, ), Param( key="CENSUS_REPORTING_ENABLED", @@ -822,9 +796,7 @@ placeholder="{'queue_name': 'queue_name'}", required=False, field=forms.JSONField, - field_kwargs={ - "widget": forms.Textarea(attrs={"class": "vLargeTextField"}) - }, + field_kwargs={"widget": forms.Textarea(attrs={"class": "vLargeTextField"})}, ), Param( key="RELEASE_CHECK_URL", diff --git a/netbox_cloud_pilot/forms.py b/netbox_cloud_pilot/forms.py index 0d14773..2ee2ca2 100644 --- a/netbox_cloud_pilot/forms.py +++ b/netbox_cloud_pilot/forms.py @@ -4,7 +4,7 @@ from django.forms import ValidationError from netbox.forms import NetBoxModelForm -from utilities.forms import BootstrapMixin +from utilities.forms import BootstrapMixin, ConfirmationForm as _ConfirmationForm from utilities.forms.fields import CommentField from .constants import NETBOX_SETTINGS, NODE_GROUP_SQLDB from .models import * @@ -98,8 +98,7 @@ def __init__(self, *args, **kwargs): f"{env_info['env']['displayName']} ({env_info['env']['envName']})", ) for env_info in env_infos - if env_info.get("env", {}).get("properties", {}).get("projectScope", "") - == "backup" + if env_info.get("env", {}).get("properties", {}).get("projectScope", "") == "backup" ] @@ -107,11 +106,7 @@ class NetBoxSettingsForm(BootstrapMixin, forms.Form): fieldsets = create_fieldset() class Meta: - fields = [ - param.key.lower() - for section in NETBOX_SETTINGS.sections - for param in section.params - ] + fields = [param.key.lower() for section in NETBOX_SETTINGS.sections for param in section.params] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -200,11 +195,7 @@ def __init__(self, *args, **kwargs): # Fetch the region list and build the choices nc = NetBoxConfiguration.objects.first() - regions = ( - nc.iaas(nc.env_name, auto_init=False) - .client.environment.Control.GetRegions() - .get("array", []) - ) + regions = nc.iaas(nc.env_name, auto_init=False).client.environment.Control.GetRegions().get("array", []) self.fields["region"].choices = [ (hard_node_group["uniqueName"], region["displayName"]) for region in regions @@ -216,17 +207,11 @@ def clean(self): if cleaned_data["deployment"] == "cluster": if cleaned_data["node_count"] == "1": - raise ValidationError( - { - "node_count": "Node count must be greater than 1 for a cluster deployment." - } - ) + raise ValidationError({"node_count": "Node count must be greater than 1 for a cluster deployment."}) if cleaned_data["deployment"] == "standalone": if cleaned_data["node_count"] != "1": - raise ValidationError( - {"node_count": "Node count must be 1 for a standalone deployment."} - ) + raise ValidationError({"node_count": "Node count must be 1 for a standalone deployment."}) return cleaned_data @@ -254,9 +239,7 @@ def __init__(self, *args, **kwargs): if self.instance.pk: # Fetch the database password from the addon settings - app = self.instance.netbox_env.get_env().get_installed_addon( - "db-backup", node_group=NODE_GROUP_SQLDB - ) + app = self.instance.netbox_env.get_env().get_installed_addon("db-backup", node_group=NODE_GROUP_SQLDB) data = app.get("settings", {}).get("main", {}).get("data", {}) self.fields["db_password"].initial = data.get("dbpass") @@ -267,9 +250,7 @@ def clean(self): self.instance._db_password = db_password if not self.instance.pk and not db_password: - raise ValidationError( - {"db_password": "This field is required when adding a new backup."} - ) + raise ValidationError({"db_password": "This field is required when adding a new backup."}) class NetBoxPluginInstallForm(BootstrapMixin, forms.Form): @@ -308,9 +289,7 @@ def __init__(self, *args, **kwargs): # Get the plugins.yaml plugins = get_plugins_list() if plugin := plugins.get(initial.get("name")): - self.fields["version"].choices = [ - (release, release) for release in filter_releases(plugin) - ] + self.fields["version"].choices = [(release, release) for release in filter_releases(plugin)] if initial.get("type") == "update": from importlib.metadata import metadata @@ -322,14 +301,13 @@ def __init__(self, *args, **kwargs): def clean(self): plugins = get_plugins_list() plugin = plugins.get(self.cleaned_data.get("name")) + selected_version = self.cleaned_data.get("version") # If the plugin is private, ensure that license is provided nc = NetBoxConfiguration.objects.first() if plugin.get("private"): if not nc.license: - raise ValidationError( - {"name": "This plugin requires a NetBox Enterprise license."} - ) + raise ValidationError({"name": "This plugin requires a NetBox Enterprise license."}) # Check if the plugin is accessible using the license response = requests.get( @@ -337,23 +315,22 @@ def clean(self): headers={"Authorization": f"Bearer {nc.license}"}, ) if not response.ok: - raise ValidationError( - { - "name": "This plugin is not accessible using the provided license." - } - ) + raise ValidationError({"name": "This plugin is not accessible using the provided license."}) # Get the required_settings from the plugin - required_settings = plugin.get("required_settings", []) + required_settings = next( + ( + release.get("netbox", {}).get("required_settings", []) + for release in plugin.get("releases", []) + if release.get("tag") == selected_version + ), + [], + ) configuration = self.cleaned_data.get("configuration") # Check if the required_settings are in the configuration if not all(key in configuration.keys() for key in required_settings): - raise ValidationError( - { - "configuration": f"Missing required settings: {', '.join(required_settings)}" - } - ) + raise ValidationError({"configuration": f"Missing required settings: {', '.join(required_settings)}"}) class NetBoxUpgradeForm(BootstrapMixin, forms.Form): @@ -371,7 +348,24 @@ def __init__(self, *args, **kwargs): instance = NetBoxConfiguration.objects.first() env = instance.get_env() - versions = env.get_patch_upgrades() - self.fields["version"].choices = [ - (str(version), str(version)) for version in versions - ] + versions = env.get_upgrades() + self.fields["version"].choices = [(str(version), str(version)) for version in versions] + + def clean(self): + super().clean() + + instance = NetBoxConfiguration.objects.first() + env = instance.get_env() + + # Run upgrade checks + upgrade_check, error = env.upgrade_checks(self.cleaned_data.get("version")) + if not upgrade_check: + raise ValidationError({"version": error}) + + +class ConfirmationForm(_ConfirmationForm): + """ + A generic confirmation form. The form is not valid unless the `confirm` field is checked. + """ + + name = forms.CharField(widget=forms.HiddenInput()) diff --git a/netbox_cloud_pilot/iaas.py b/netbox_cloud_pilot/iaas.py index 0c2eae5..8e6d5b2 100644 --- a/netbox_cloud_pilot/iaas.py +++ b/netbox_cloud_pilot/iaas.py @@ -1,6 +1,7 @@ import logging import time from functools import lru_cache +from typing import Tuple import requests import yaml @@ -12,6 +13,7 @@ from core.choices import JobStatusChoices from core.models import Job from netbox.config import get_config +from . import utils from .constants import JELASTIC_API, NODE_GROUP_CP, NODE_GROUP_SQLDB logger = logging.getLogger("netbox_cloud_pilot") @@ -116,9 +118,7 @@ def get_nodes(self, node_group: str = None, is_master: bool = True) -> dict | li """ Get the environment nodes. """ - logger.debug( - f"Getting nodes for node group {node_group}, is_master={is_master}" - ) + logger.debug(f"Getting nodes for node group {node_group}, is_master={is_master}") nodes = self._get_env_info().get("nodes", []) if node_group: nodes = [node for node in nodes if node["nodeGroup"] == node_group] @@ -137,9 +137,7 @@ def get_node_groups(self): # For each node group, get the related nodes for node_group in node_groups: - node_group["node"] = self.get_nodes( - node_group=node_group["name"], is_master=True - ) + node_group["node"] = self.get_nodes(node_group=node_group["name"], is_master=True) return node_groups @@ -162,9 +160,7 @@ def is_ssl_enabled(self): """ Check if SSL is enabled. """ - logger.debug( - f"Checking if built-in SSL is enabled for environment {self.env_name}" - ) + logger.debug(f"Checking if built-in SSL is enabled for environment {self.env_name}") return self.get_env().get("sslstate", False) def get_node_log(self, node_id, path="/var/log/run.log"): @@ -172,9 +168,9 @@ def get_node_log(self, node_id, path="/var/log/run.log"): Get the logs of a node. """ logger.debug(f"Getting log for node {node_id} for environment {self.env_name}") - return self.client.environment.Control.ReadLog( - env_name=self.env_name, node_id=node_id, path=path - ).get("body", "") + return self.client.environment.Control.ReadLog(env_name=self.env_name, node_id=node_id, path=path).get( + "body", "" + ) def get_url(self): """ @@ -201,17 +197,11 @@ def get_installed_addon(self, app_id, node_group, search=None): """ Get the installed addon for a node group. """ - logger.debug( - f"Checking if addon ({app_id}) is installed for node group {node_group}" - ) + logger.debug(f"Checking if addon ({app_id}) is installed for node group {node_group}") addons = self.get_addons(node_group=node_group, search=search) return next( - ( - addon - for addon in addons - if addon["app_id"] == app_id and addon.get("isInstalled", False) - ), + (addon for addon in addons if addon["app_id"] == app_id and addon.get("isInstalled", False)), None, ) @@ -257,16 +247,12 @@ def run_script(self, name, code, description=None, params=None): for script in scripts: if script.get("name") == name: logger.debug(f"Deleting existing script {name}") - self.client.development.Scripting.DeleteScript( - app_id=app_id, name=name - ) + self.client.development.Scripting.DeleteScript(app_id=app_id, name=name) continue except JelasticApiError as e: logger.error(e) - self.client.development.Scripting.CreateScript( - app_id=app_id, name=name, type="js", code=code - ) + self.client.development.Scripting.CreateScript(app_id=app_id, name=name, type="js", code=code) return self.client.utils.Scheduler.CreateEnvTask( env_name=self.env_name, @@ -276,9 +262,7 @@ def run_script(self, name, code, description=None, params=None): params=params, ) - def restart_nodes( - self, node_groups: list[str], lazy: bool = False, delay: int = 10000 - ): + def restart_nodes(self, node_groups: list[str], lazy: bool = False, delay: int = 10000): """ Restart nodes for a list of node groups. """ @@ -367,9 +351,7 @@ def uninstall_addon(self, app_id, node_group, search=None): Uninstall an addon. """ # Check if the addon is already installed - if addon := self.get_installed_addon( - app_id=app_id, node_group=node_group, search=search - ): + if addon := self.get_installed_addon(app_id=app_id, node_group=node_group, search=search): logger.info(f"Uninstalling addon {app_id}") return self.client.marketplace.Installation.Uninstall( @@ -401,9 +383,7 @@ def get_nb_node_groups(self): # For each node group, get the related nodes for node_group in node_groups: - if "netbox" in node_group.get("node", {}).get("customitem", {}).get( - "dockerName" - ): + if "netbox" in node_group.get("node", {}).get("customitem", {}).get("dockerName"): results.append(node_group) return results @@ -413,9 +393,7 @@ def load_plugins(self): Loads the plugins from the plugins.yaml file. """ master_node_id = self.get_master_node(NODE_GROUP_CP).get("id") - plugins_yaml = self.execute_cmd( - master_node_id, "cat /etc/netbox/config/plugins.yaml" - )[0].get("out", "") + plugins_yaml = self.execute_cmd(master_node_id, "cat /etc/netbox/config/plugins.yaml")[0].get("out", "") return yaml.safe_load(plugins_yaml) or {} def dump_plugins(self, plugins): @@ -433,22 +411,19 @@ def dump_plugins(self, plugins): is_append_mode=False, ) - def install_plugin( - self, plugin: dict, version, plugin_settings=None, github_token=None - ): + def install_plugin(self, plugin: dict, version, plugin_settings=None, github_token=None): master_node_id = self.get_master_node(NODE_GROUP_CP).get("id") activate_env = "source /opt/netbox/venv/bin/activate" # Install the plugin version if plugin.get("private"): + # Ensure `git` is installed + self.execute_cmd(master_node_id, "apt-get install -y git") + github_url = plugin.get("github_url") - github_url = github_url.replace( - "https://github.com", f"git+https://{github_token}@github.com" - ) + github_url = github_url.replace("https://github.com", f"git+https://{github_token}@github.com") - self.execute_cmd( - master_node_id, f"{activate_env} && pip install {github_url}@{version}" - ) + self.execute_cmd(master_node_id, f"{activate_env} && pip install {github_url}@{version}") else: self.execute_cmd( master_node_id, @@ -459,17 +434,14 @@ def install_plugin( plugins[plugin.get("app_label")] = plugin_settings or {} self.dump_plugins(plugins) - # TODO: Uncomment this after a fix is suggested by Virtuozzo, slack thread: https://omsmsp.slack.com/archives/C05QT7WD71U/p1704140085788849 # Run collectstatic command - # self.execute_cmd( - # node_id=master_node_id, - # command=f"{activate_env} && /opt/netbox/netbox/manage.py collectstatic --no-input", - # ) + self.execute_cmd( + node_id=master_node_id, + command=f"{activate_env} && /opt/netbox/netbox/manage.py collectstatic --no-input --clear 1>/dev/null", + ) return self.restart_nodes( - node_groups=[ - node_group["name"] for node_group in self.get_nb_node_groups() - ], + node_groups=[node_group["name"] for node_group in self.get_nb_node_groups()], lazy=True, ) @@ -482,9 +454,7 @@ def uninstall_plugin(self, plugin: dict): self.dump_plugins(plugins) return self.restart_nodes( - node_groups=[ - node_group["name"] for node_group in self.get_nb_node_groups() - ], + node_groups=[node_group["name"] for node_group in self.get_nb_node_groups()], lazy=True, ) @@ -493,10 +463,7 @@ def get_env_var(self, variable, default=None): Get the environment variable for NetBox. """ container_vars = self._get_env_var(NODE_GROUP_CP) - return ( - getattr(get_config(), variable, container_vars.get(variable, None)) - or default - ) + return getattr(get_config(), variable, container_vars.get(variable, None)) or default def _get_docker_tags(self): """ @@ -505,9 +472,7 @@ def _get_docker_tags(self): master_node = self.get_master_node(NODE_GROUP_CP) docker = master_node.get("customitem", {}) - response = requests.get( - f'https://hub.docker.com/v2/repositories/{docker["dockerName"]}/tags?page_size=1000' - ) + response = requests.get(f'https://hub.docker.com/v2/repositories/{docker["dockerName"]}/tags?page_size=1000') response.raise_for_status() response = response.json() @@ -532,68 +497,15 @@ def _get_upgrades(self): return [tag for tag in docker_tags if tag > current_version] - def get_patch_upgrades(self): - """ - Get the available patch upgrades for NetBox. - """ - all_upgrades = self._get_upgrades() - current_version = Version.parse(settings.VERSION) - - # Filter out only the patch upgrades - patch_upgrades = [] - for upgrade in all_upgrades: - if ( - upgrade.major == current_version.major - and upgrade.minor == current_version.minor - ): - patch_upgrades.append(upgrade) - - return patch_upgrades - - def get_minor_upgrades(self): - """ - Get the available minor upgrades for NetBox. - """ - all_upgrades = self._get_upgrades() - current_version = Version.parse(settings.VERSION) - - # Filter out only the minor upgrades - minor_upgrades = [] - for upgrade in all_upgrades: - if ( - upgrade.major == current_version.major - and upgrade.minor > current_version.minor - ): - minor_upgrades.append(upgrade) - - return minor_upgrades - - def get_major_upgrades(self): - """ - Get the available major upgrades for NetBox. - """ - all_upgrades = self._get_upgrades() - current_version = Version.parse(settings.VERSION) - - # Filter out only the major upgrades - major_upgrades = [] - for upgrade in all_upgrades: - if upgrade.major > current_version.major: - major_upgrades.append(upgrade) - - return major_upgrades - def is_upgrade_available(self): """ Check if an upgrade is available for NetBox. """ - return bool(self._get_upgrades()) + return bool(self.get_upgrades()) def is_db_backup_running(self, app_unique_name): # Get current running actions - current_actions = self.client.environment.Tracking.GetCurrentActions().get( - "array", [] - ) + current_actions = self.client.environment.Tracking.GetCurrentActions().get("array", []) for action in current_actions: action_parameters = action.get("parameters", {}) @@ -615,19 +527,31 @@ def db_backup(self, app_unique_name): return self.execute_action(app_unique_name=app_unique_name, action="backup") + def upgrade_checks(self, version) -> Tuple[bool, str]: + """ + Run upgrade checks for NetBox. + """ + # Fetch the plugins from the store + plugins = utils.get_plugins_list() + for plugin_name, plugin in plugins.items(): + if plugin_name in settings.PLUGINS: + if not utils.filter_releases(plugin, version): + return ( + False, + f"Plugin {plugin_name} does not have a release for version {version}", + ) + + return True, "" + def upgrade(self, version): """ Upgrade NetBox. """ version = f"v{version}" - if addon := self.get_installed_addon( - app_id="db-backup", node_group=NODE_GROUP_SQLDB - ): + if addon := self.get_installed_addon(app_id="db-backup", node_group=NODE_GROUP_SQLDB): self.db_backup(app_unique_name=addon.get("uniqueName")) - # TODO: Run plugin compatibility checks - # Fetch all node groups for node_group in self.get_nb_node_groups(): node_group_name = node_group["name"] diff --git a/netbox_cloud_pilot/models.py b/netbox_cloud_pilot/models.py index 2918e0f..631a13a 100644 --- a/netbox_cloud_pilot/models.py +++ b/netbox_cloud_pilot/models.py @@ -35,9 +35,7 @@ class NetBoxConfiguration(JobsMixin, PrimaryModel): NetBoxConfig is a model that represents the configuration of NetBox. """ - key = models.CharField( - max_length=255, unique=True, validators=[MinLengthValidator(40)] - ) + key = models.CharField(max_length=255, unique=True, validators=[MinLengthValidator(40)]) env_name = models.CharField( max_length=255, @@ -112,12 +110,7 @@ def get_env(self): return self.iaas(self.env_name) def get_docker_tag(self): - return ( - self.get_env() - .get_master_node(NODE_GROUP_CP) - .get("customitem", {}) - .get("dockerTag", "") - ) + return self.get_env().get_master_node(NODE_GROUP_CP).get("customitem", {}).get("dockerTag", "") def get_env_storage(self): return self.iaas(self.env_name_storage) @@ -151,9 +144,7 @@ def netbox_settings(self): initial = env.get_env_var(param.key, param.initial) # Alter list of strings to comma-separated string - if isinstance(initial, (tuple, list)) and all( - [isinstance(x, str) for x in initial] - ): + if isinstance(initial, (tuple, list)) and all([isinstance(x, str) for x in initial]): initial = ", ".join(initial) param.initial = initial @@ -168,11 +159,7 @@ def apply_settings(self, data: dict): """ logger.debug(f"Applying NetBox settings: {data}") - all_keys = [ - param.key - for section in NETBOX_SETTINGS.sections - for param in section.params - ] + all_keys = [param.key for section in NETBOX_SETTINGS.sections for param in section.params] env = self.get_env() # Get all NetBox node groups @@ -329,20 +316,14 @@ def clean(self): # Ensure netbox_env has a storage env connected before proceeding if not self.netbox_env.env_name_storage: raise ValidationError( - { - "netbox_env": "Add a backup storage environment to the NetBoxConfiguration instance." - } + {"netbox_env": "Add a backup storage environment to the NetBoxConfiguration instance."} ) if self.keep_backups > 30: - raise ValidationError( - {"keep_backups": "The maximum number of backups to keep is 30."} - ) + raise ValidationError({"keep_backups": "The maximum number of backups to keep is 30."}) if self.keep_backups < 1: - raise ValidationError( - {"keep_backups": "The minimum number of backups to keep is 1."} - ) + raise ValidationError({"keep_backups": "The minimum number of backups to keep is 1."}) try: croniter(self.crontab) @@ -367,11 +348,7 @@ def list_backups(self): node_id=master_node.get("id"), command="/root/getBackupsAllEnvs.sh" )[0] - if ( - backups := json.loads(result.get("out", "")) - .get("backups", {}) - .get(self.netbox_env.env_name, []) - ): + if backups := json.loads(result.get("out", "")).get("backups", {}).get(self.netbox_env.env_name, []): # Cache backups in case next time it returns empty cache.set(f"netbox_db_backups_{self.pk}", backups, timeout=60 * 60) else: @@ -420,9 +397,7 @@ def backup(self, request=None): Create a new database backup. """ # Get installed addon - addon = self.netbox_env.get_env().get_installed_addon( - app_id="db-backup", node_group=NODE_GROUP_SQLDB - ) + addon = self.netbox_env.get_env().get_installed_addon(app_id="db-backup", node_group=NODE_GROUP_SQLDB) return self.netbox_env.enqueue( self.netbox_env.get_env().db_backup, @@ -435,9 +410,7 @@ def restore(self, request, backup_name): Restore a database backup. """ # Get installed addon - addon = self.netbox_env.get_env().get_installed_addon( - app_id="db-backup", node_group=NODE_GROUP_SQLDB - ) + addon = self.netbox_env.get_env().get_installed_addon(app_id="db-backup", node_group=NODE_GROUP_SQLDB) return self.netbox_env.enqueue( self.netbox_env.get_env().execute_action, diff --git a/netbox_cloud_pilot/nb_settings.py b/netbox_cloud_pilot/nb_settings.py index 0edfcf1..388ee73 100644 --- a/netbox_cloud_pilot/nb_settings.py +++ b/netbox_cloud_pilot/nb_settings.py @@ -63,9 +63,7 @@ def __init__( if widget and hasattr(widget, "attrs"): # Update the class attribute, retaining existing classes existing_classes = widget.attrs.get("class", "") - widget.attrs["class"] = " ".join( - filter(None, [existing_classes, "form-control"]) - ) + widget.attrs["class"] = " ".join(filter(None, [existing_classes, "form-control"])) else: # If widget is not provided or doesn't have attrs, use the default field_kwargs.update(default_kwargs) diff --git a/netbox_cloud_pilot/templates/netbox_cloud_pilot/inc/plugin_store.html b/netbox_cloud_pilot/templates/netbox_cloud_pilot/inc/plugin_store.html new file mode 100644 index 0000000..6b23a13 --- /dev/null +++ b/netbox_cloud_pilot/templates/netbox_cloud_pilot/inc/plugin_store.html @@ -0,0 +1,47 @@ +
{{ plugin.description }}
+ {% if plugin.github_url and not plugin.private %} + + View on GitHub + + {% endif %} + {% if plugin.pypi_url %} + + View on PyPI + + {% endif %} +{{ plugin.description }}
- {% if plugin.github_url %} - - View on GitHub - - {% endif %} - {% if plugin.pypi_url %} - - View on PyPI - - {% endif %} -